diff --git a/src/PowderCoating.Application/DTOs/Invoice/InvoiceDtos.cs b/src/PowderCoating.Application/DTOs/Invoice/InvoiceDtos.cs
index d9eddb7..e3699db 100644
--- a/src/PowderCoating.Application/DTOs/Invoice/InvoiceDtos.cs
+++ b/src/PowderCoating.Application/DTOs/Invoice/InvoiceDtos.cs
@@ -32,7 +32,9 @@ public class InvoiceDto
public string CustomerName { get; set; } = string.Empty;
public string? CustomerEmail { get; set; }
public string? CustomerPhone { get; set; }
+ public string? CustomerMobilePhone { get; set; }
public bool CustomerNotifyByEmail { get; set; }
+ public bool CustomerNotifyBySms { get; set; }
public string? PreparedById { get; set; }
public string? PreparedByName { get; set; }
public InvoiceStatus Status { get; set; }
diff --git a/src/PowderCoating.Application/DTOs/Kiosk/KioskDtos.cs b/src/PowderCoating.Application/DTOs/Kiosk/KioskDtos.cs
new file mode 100644
index 0000000..2dfb3ae
--- /dev/null
+++ b/src/PowderCoating.Application/DTOs/Kiosk/KioskDtos.cs
@@ -0,0 +1,87 @@
+using System.ComponentModel.DataAnnotations;
+using PowderCoating.Core.Enums;
+
+namespace PowderCoating.Application.DTOs.Kiosk;
+
+// ── Staff-facing ──────────────────────────────────────────────────────────────
+
+/// Input for sending a remote intake link to a customer by email.
+public class SendRemoteLinkDto
+{
+ [Required, EmailAddress]
+ public string Email { get; set; } = string.Empty;
+
+ /// Optional — used to personalise the email greeting.
+ public string? CustomerName { get; set; }
+}
+
+// ── Customer-facing step DTOs ─────────────────────────────────────────────────
+
+/// Step 1 — Contact information submitted by the customer.
+public class SubmitKioskContactDto
+{
+ [Required, MaxLength(100)]
+ public string FirstName { get; set; } = string.Empty;
+
+ [Required, MaxLength(100)]
+ public string LastName { get; set; } = string.Empty;
+
+ [Required, Phone]
+ public string Phone { get; set; } = string.Empty;
+
+ [Required, EmailAddress]
+ public string Email { get; set; } = string.Empty;
+
+ public bool IsReturningCustomer { get; set; }
+}
+
+/// Step 2 — Job description submitted by the customer.
+public class SubmitKioskJobDto
+{
+ [Required, MaxLength(2000)]
+ public string JobDescription { get; set; } = string.Empty;
+
+ public string? HowDidYouHearAboutUs { get; set; }
+}
+
+/// Step 3 — Terms agreement (+ optional drawn signature for in-person sessions).
+public class SubmitKioskTermsDto
+{
+ [Required]
+ [Range(typeof(bool), "true", "true", ErrorMessage = "You must agree to the terms to continue.")]
+ public bool AgreedToTerms { get; set; }
+
+ public bool SmsOptIn { get; set; }
+
+ /// Base-64 PNG from signature_pad; required for InPerson sessions, null for Remote.
+ public string? SignatureDataBase64 { get; set; }
+}
+
+// ── Staff review list ─────────────────────────────────────────────────────────
+
+/// One row in the Kiosk Intakes staff review list.
+public class KioskSessionListDto
+{
+ public int Id { get; set; }
+ public Guid SessionToken { get; set; }
+ public KioskSessionType SessionType { get; set; }
+ public KioskSessionStatus Status { get; set; }
+ public string CustomerFirstName { get; set; } = string.Empty;
+ public string CustomerLastName { get; set; } = string.Empty;
+ public string CustomerEmail { get; set; } = string.Empty;
+ public string CustomerPhone { get; set; } = string.Empty;
+ public string JobDescription { get; set; } = string.Empty;
+ public bool SmsOptIn { get; set; }
+ public DateTime? SubmittedAt { get; set; }
+ public DateTime ExpiresAt { get; set; }
+ public int? LinkedCustomerId { get; set; }
+ public int? LinkedJobId { get; set; }
+ public string? RemoteLinkEmail { get; set; }
+
+ public string CustomerFullName => $"{CustomerFirstName} {CustomerLastName}".Trim();
+ public string JobDescriptionSnippet =>
+ JobDescription.Length > 80 ? JobDescription[..80] + "…" : JobDescription;
+ public bool IsConverted => LinkedJobId.HasValue;
+ public bool IsExpired => Status == KioskSessionStatus.Expired ||
+ (Status == KioskSessionStatus.Active && DateTime.UtcNow > ExpiresAt);
+}
diff --git a/src/PowderCoating.Application/Interfaces/INotificationService.cs b/src/PowderCoating.Application/Interfaces/INotificationService.cs
index 24a014b..bd30f19 100644
--- a/src/PowderCoating.Application/Interfaces/INotificationService.cs
+++ b/src/PowderCoating.Application/Interfaces/INotificationService.cs
@@ -58,7 +58,7 @@ public interface INotificationService
/// Notify customer when an invoice has been sent.
/// Optionally includes an online payment link in the email body.
///
- Task NotifyInvoiceSentAsync(Invoice invoice, byte[]? pdfAttachment = null, string? pdfFilename = null, string? paymentUrl = null, string? overrideEmail = null);
+ Task NotifyInvoiceSentAsync(Invoice invoice, byte[]? pdfAttachment = null, string? pdfFilename = null, string? paymentUrl = null, string? overrideEmail = null, bool sendSms = false, string? viewUrl = null);
///
/// Notify customer (internal) when a payment has been recorded on an invoice.
diff --git a/src/PowderCoating.Application/Mappings/InvoiceProfile.cs b/src/PowderCoating.Application/Mappings/InvoiceProfile.cs
index 6657989..419b780 100644
--- a/src/PowderCoating.Application/Mappings/InvoiceProfile.cs
+++ b/src/PowderCoating.Application/Mappings/InvoiceProfile.cs
@@ -28,7 +28,9 @@ public class InvoiceProfile : Profile
? (s.Customer.BillingEmail ?? s.Customer.Email)
: null))
.ForMember(d => d.CustomerPhone, o => o.MapFrom(s => s.Customer != null ? s.Customer.Phone : null))
+ .ForMember(d => d.CustomerMobilePhone, o => o.MapFrom(s => s.Customer != null ? s.Customer.MobilePhone : null))
.ForMember(d => d.CustomerNotifyByEmail, o => o.MapFrom(s => s.Customer == null || s.Customer.NotifyByEmail))
+ .ForMember(d => d.CustomerNotifyBySms, o => o.MapFrom(s => s.Customer != null && s.Customer.NotifyBySms))
.ForMember(d => d.PreparedByName, o => o.MapFrom(s => s.PreparedBy != null
? $"{s.PreparedBy.FirstName} {s.PreparedBy.LastName}".Trim()
: null))
diff --git a/src/PowderCoating.Core/Entities/Company.cs b/src/PowderCoating.Core/Entities/Company.cs
index 0b357af..1d5552c 100644
--- a/src/PowderCoating.Core/Entities/Company.cs
+++ b/src/PowderCoating.Core/Entities/Company.cs
@@ -123,6 +123,16 @@ public class Company : BaseEntity
public byte[]? LogoData { get; set; } // Legacy - kept for backward compatibility
public string? LogoContentType { get; set; } // Legacy - kept for backward compatibility
public string? LogoFilePath { get; set; } // Filesystem path: /media/{CompanyId}/company-logo.{ext}
+
+ // Kiosk
+ ///
+ /// Random token written to a long-lived HttpOnly cookie on the front-desk tablet when the
+ /// owner activates the kiosk. Kiosk routes validate this token against the cookie so the
+ /// tablet can serve the intake form without requiring a logged-in user.
+ /// Null = kiosk not activated. Regenerate to revoke the current device.
+ ///
+ public string? KioskActivationToken { get; set; }
+
// Navigation Properties
public virtual ICollection Users { get; set; } = new List();
public virtual ICollection Customers { get; set; } = new List();
diff --git a/src/PowderCoating.Core/Entities/Invoice.cs b/src/PowderCoating.Core/Entities/Invoice.cs
index 7573ea0..d3a0a02 100644
--- a/src/PowderCoating.Core/Entities/Invoice.cs
+++ b/src/PowderCoating.Core/Entities/Invoice.cs
@@ -28,6 +28,13 @@ public class Invoice : BaseEntity
public decimal GiftCertificateRedeemed { get; set; } // Sum of gift certificate redemptions
public decimal BalanceDue => Total - AmountPaid - CreditApplied - GiftCertificateRedeemed;
+ ///
+ /// Permanent public token for the customer-facing invoice view page (/invoice/{token}).
+ /// Generated when the invoice is first sent (regardless of Stripe status) and never expires.
+ /// Distinct from PaymentLinkToken which is Stripe-gated and expires in 5 days.
+ ///
+ public string? PublicViewToken { get; set; }
+
// Online payments (Stripe Connect)
public OnlinePaymentStatus OnlinePaymentStatus { get; set; } = OnlinePaymentStatus.NotApplicable;
public string? PaymentLinkToken { get; set; } // Signed token for /pay/{token}
diff --git a/src/PowderCoating.Core/Entities/KioskSession.cs b/src/PowderCoating.Core/Entities/KioskSession.cs
new file mode 100644
index 0000000..5455821
--- /dev/null
+++ b/src/PowderCoating.Core/Entities/KioskSession.cs
@@ -0,0 +1,51 @@
+using PowderCoating.Core.Enums;
+
+namespace PowderCoating.Core.Entities;
+
+///
+/// Represents one customer self-service intake session — either completed on the front-desk tablet
+/// (InPerson) or via an emailed link the customer fills out on their own device (Remote).
+/// Sessions are tenant-scoped and soft-deletable. Load anonymous sessions with ignoreQueryFilters:true.
+///
+public class KioskSession : BaseEntity
+{
+ /// URL-safe GUID used in all kiosk routes; unique across the table.
+ public Guid SessionToken { get; set; } = Guid.NewGuid();
+
+ public KioskSessionType SessionType { get; set; }
+ public KioskSessionStatus Status { get; set; } = KioskSessionStatus.Active;
+
+ // ── Step 1 — Contact ─────────────────────────────────────────────────────
+ public string CustomerFirstName { get; set; } = string.Empty;
+ public string CustomerLastName { get; set; } = string.Empty;
+ public string CustomerPhone { get; set; } = string.Empty;
+ public string CustomerEmail { get; set; } = string.Empty;
+ public bool IsReturningCustomer { get; set; }
+
+ // ── Step 2 — Job Description ──────────────────────────────────────────────
+ public string JobDescription { get; set; } = string.Empty;
+ public string? HowDidYouHearAboutUs { get; set; }
+
+ // ── Step 3 — Terms & Consent ──────────────────────────────────────────────
+ public bool AgreedToTerms { get; set; }
+ public DateTime? AgreedToTermsAt { get; set; }
+ /// Customer opted in to SMS order updates; sets Customer.NotifyBySms on submission.
+ public bool SmsOptIn { get; set; }
+ /// Base-64 PNG from signature_pad; null for Remote sessions (no drawn signature required).
+ public string? SignatureDataBase64 { get; set; }
+
+ // ── Outcome ───────────────────────────────────────────────────────────────
+ public int? LinkedCustomerId { get; set; }
+ public int? LinkedJobId { get; set; }
+ public DateTime? SubmittedAt { get; set; }
+ /// Sessions auto-expire 2 h after creation (InPerson) or 48 h (Remote). ExpiresAt is set at creation.
+ public DateTime ExpiresAt { get; set; }
+
+ // ── Remote-only ───────────────────────────────────────────────────────────
+ public string? RemoteLinkEmail { get; set; }
+ public DateTime? RemoteLinkSentAt { get; set; }
+
+ // ── Navigation ────────────────────────────────────────────────────────────
+ public virtual Customer? LinkedCustomer { get; set; }
+ public virtual Job? LinkedJob { get; set; }
+}
diff --git a/src/PowderCoating.Core/Enums/KioskEnums.cs b/src/PowderCoating.Core/Enums/KioskEnums.cs
new file mode 100644
index 0000000..5bcf4b4
--- /dev/null
+++ b/src/PowderCoating.Core/Enums/KioskEnums.cs
@@ -0,0 +1,15 @@
+namespace PowderCoating.Core.Enums;
+
+public enum KioskSessionType
+{
+ InPerson = 0,
+ Remote = 1
+}
+
+public enum KioskSessionStatus
+{
+ Active = 0,
+ Submitted = 1,
+ Expired = 2,
+ Cancelled = 3
+}
diff --git a/src/PowderCoating.Core/Interfaces/IUnitOfWork.cs b/src/PowderCoating.Core/Interfaces/IUnitOfWork.cs
index 224ff68..791d852 100644
--- a/src/PowderCoating.Core/Interfaces/IUnitOfWork.cs
+++ b/src/PowderCoating.Core/Interfaces/IUnitOfWork.cs
@@ -154,6 +154,9 @@ public interface IUnitOfWork : IDisposable
IRepository GiftCertificates { get; }
IRepository GiftCertificateRedemptions { get; }
+ // Customer Intake Kiosk
+ IRepository KioskSessions { get; }
+
Task SaveChangesAsync();
Task CompleteAsync(); // Alias for SaveChangesAsync
diff --git a/src/PowderCoating.Infrastructure/Data/ApplicationDbContext.cs b/src/PowderCoating.Infrastructure/Data/ApplicationDbContext.cs
index 65e95d0..b4482ae 100644
--- a/src/PowderCoating.Infrastructure/Data/ApplicationDbContext.cs
+++ b/src/PowderCoating.Infrastructure/Data/ApplicationDbContext.cs
@@ -367,6 +367,10 @@ public class ApplicationDbContext : IdentityDbContext, IDataPro
/// Prep-service definitions within a job template item.
public DbSet JobTemplateItemPrepServices { get; set; }
+ // Customer Intake Kiosk
+ /// Customer self-service intake sessions (walk-in tablet or remote email link); tenant-filtered with soft delete.
+ public DbSet KioskSessions { get; set; }
+
///
/// Platform-wide audit log capturing who changed what and when, across all tenants.
/// No global query filter — SuperAdmin controllers query this directly.
@@ -746,6 +750,24 @@ public class ApplicationDbContext : IdentityDbContext, IDataPro
modelBuilder.Entity().HasQueryFilter(e =>
!e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
+ // Customer intake kiosk sessions — tenant-filtered + soft delete.
+ // Anonymous intake routes must use ignoreQueryFilters:true when loading by SessionToken.
+ modelBuilder.Entity().HasQueryFilter(e =>
+ !e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
+ modelBuilder.Entity()
+ .HasIndex(e => e.SessionToken)
+ .IsUnique();
+ modelBuilder.Entity()
+ .HasOne(k => k.LinkedCustomer)
+ .WithMany()
+ .HasForeignKey(k => k.LinkedCustomerId)
+ .OnDelete(DeleteBehavior.SetNull);
+ modelBuilder.Entity()
+ .HasOne(k => k.LinkedJob)
+ .WithMany()
+ .HasForeignKey(k => k.LinkedJobId)
+ .OnDelete(DeleteBehavior.SetNull);
+
// Account self-referencing hierarchy
modelBuilder.Entity()
.HasOne(a => a.ParentAccount)
diff --git a/src/PowderCoating.Infrastructure/Data/SeedData.cs b/src/PowderCoating.Infrastructure/Data/SeedData.cs
index 08a13c5..850cc6e 100644
--- a/src/PowderCoating.Infrastructure/Data/SeedData.cs
+++ b/src/PowderCoating.Infrastructure/Data/SeedData.cs
@@ -967,6 +967,17 @@ New accounts walk through an 18-step setup wizard to configure company informati
CreatedAt = DateTime.UtcNow
},
new NotificationTemplate
+ {
+ NotificationType = NotificationType.InvoiceSent,
+ Channel = NotificationChannel.Sms,
+ DisplayName = "Invoice Sent (SMS)",
+ Subject = null,
+ Body = "{{companyName}}: Invoice {{invoiceNumber}} for {{invoiceTotal}} is ready. View your invoice: {{viewUrl}} Reply STOP to opt out.",
+ IsActive = true,
+ CompanyId = companyId,
+ CreatedAt = DateTime.UtcNow
+ },
+ new NotificationTemplate
{
NotificationType = NotificationType.PaymentReceived,
Channel = NotificationChannel.Email,
diff --git a/src/PowderCoating.Infrastructure/Migrations/20260513184018_AddKioskIntakeSession.Designer.cs b/src/PowderCoating.Infrastructure/Migrations/20260513184018_AddKioskIntakeSession.Designer.cs
new file mode 100644
index 0000000..99ca966
--- /dev/null
+++ b/src/PowderCoating.Infrastructure/Migrations/20260513184018_AddKioskIntakeSession.Designer.cs
@@ -0,0 +1,10732 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using PowderCoating.Infrastructure.Data;
+
+#nullable disable
+
+namespace PowderCoating.Infrastructure.Migrations
+{
+ [DbContext(typeof(ApplicationDbContext))]
+ [Migration("20260513184018_AddKioskIntakeSession")]
+ partial class AddKioskIntakeSession
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.11")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("FriendlyName")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Xml")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.ToTable("DataProtectionKeys");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Name")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("NormalizedName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedName")
+ .IsUnique()
+ .HasDatabaseName("RoleNameIndex")
+ .HasFilter("[NormalizedName] IS NOT NULL");
+
+ b.ToTable("AspNetRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ClaimValue")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RoleId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetRoleClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ClaimValue")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.Property("LoginProvider")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ProviderKey")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ProviderDisplayName")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserLogins", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("RoleId")
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetUserRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("LoginProvider")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Name")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Value")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("UserId", "LoginProvider", "Name");
+
+ b.ToTable("AspNetUserTokens", (string)null);
+ });
+
+ modelBuilder.Entity("PowderCoating.Core.Entities.Account", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AccountNumber")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("AccountSubType")
+ .HasColumnType("int");
+
+ b.Property("AccountType")
+ .HasColumnType("int");
+
+ b.Property("CompanyId")
+ .HasColumnType("int");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CurrentBalance")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("DeletedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("DeletedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Description")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("IsActive")
+ .HasColumnType("bit");
+
+ b.Property("IsDeleted")
+ .HasColumnType("bit");
+
+ b.Property("IsSystem")
+ .HasColumnType("bit");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("OpeningBalance")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("OpeningBalanceDate")
+ .HasColumnType("datetime2");
+
+ b.Property("ParentAccountId")
+ .HasColumnType("int");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ParentAccountId");
+
+ b.ToTable("Accounts");
+ });
+
+ modelBuilder.Entity("PowderCoating.Core.Entities.AiItemPrediction", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AiTags")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CompanyId")
+ .HasColumnType("int");
+
+ b.Property("Confidence")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ConversationRounds")
+ .HasColumnType("int");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("DeletedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("DeletedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("IsDeleted")
+ .HasColumnType("bit");
+
+ b.Property("PredictedComplexity")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PredictedMinutes")
+ .HasColumnType("int");
+
+ b.Property("PredictedSurfaceAreaSqFt")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("PredictedUnitPrice")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("Reasoning")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserOverrodeEstimate")
+ .HasColumnType("bit");
+
+ b.HasKey("Id");
+
+ b.ToTable("AiItemPredictions");
+ });
+
+ modelBuilder.Entity("PowderCoating.Core.Entities.AiUsageLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("CalledAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CompanyId")
+ .HasColumnType("int");
+
+ b.Property("Feature")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("InputLength")
+ .HasColumnType("int");
+
+ b.Property("Success")
+ .HasColumnType("bit");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CompanyId", "CalledAt")
+ .HasDatabaseName("IX_AiUsageLogs_CompanyId_CalledAt");
+
+ b.ToTable("AiUsageLogs");
+ });
+
+ modelBuilder.Entity("PowderCoating.Core.Entities.Announcement", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedByUserId")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CreatedByUserName")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ExpiresAt")
+ .HasColumnType("datetime2");
+
+ b.Property("IsActive")
+ .HasColumnType("bit");
+
+ b.Property("IsDismissible")
+ .HasColumnType("bit");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("StartsAt")
+ .HasColumnType("datetime2");
+
+ b.Property("Target")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TargetCompanyId")
+ .HasColumnType("int");
+
+ b.Property("TargetPlan")
+ .HasColumnType("int");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime2");
+
+ b.HasKey("Id");
+
+ b.ToTable("Announcements");
+ });
+
+ modelBuilder.Entity("PowderCoating.Core.Entities.AnnouncementDismissal", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AnnouncementId")
+ .HasColumnType("int");
+
+ b.Property("DismissedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AnnouncementId", "UserId")
+ .IsUnique();
+
+ b.ToTable("AnnouncementDismissals");
+ });
+
+ modelBuilder.Entity("PowderCoating.Core.Entities.ApplicationUser", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("int");
+
+ b.Property("Address")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("BanReason")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("BannedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("BannedByUserId")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CanApproveQuotes")
+ .HasColumnType("bit");
+
+ b.Property("CanCreateQuotes")
+ .HasColumnType("bit");
+
+ b.Property("CanManageAccounting")
+ .HasColumnType("bit");
+
+ b.Property("CanManageBills")
+ .HasColumnType("bit");
+
+ b.Property("CanManageCalendar")
+ .HasColumnType("bit");
+
+ b.Property("CanManageCustomers")
+ .HasColumnType("bit");
+
+ b.Property("CanManageEquipment")
+ .HasColumnType("bit");
+
+ b.Property("CanManageInventory")
+ .HasColumnType("bit");
+
+ b.Property("CanManageInvoices")
+ .HasColumnType("bit");
+
+ b.Property("CanManageJobs")
+ .HasColumnType("bit");
+
+ b.Property("CanManageMaintenance")
+ .HasColumnType("bit");
+
+ b.Property("CanManageProducts")
+ .HasColumnType("bit");
+
+ b.Property("CanManageVendors")
+ .HasColumnType("bit");
+
+ b.Property("CanViewCalendar")
+ .HasColumnType("bit");
+
+ b.Property("CanViewProducts")
+ .HasColumnType("bit");
+
+ b.Property("CanViewReports")
+ .HasColumnType("bit");
+
+ b.Property("CanViewShopFloor")
+ .HasColumnType("bit");
+
+ b.Property("City")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CompanyId")
+ .HasColumnType("int");
+
+ b.Property("CompanyRole")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("DashboardLayout")
+ .HasColumnType("int");
+
+ b.Property("DateFormat")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Department")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("bit");
+
+ b.Property("EmployeeNumber")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("FirstName")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("HireDate")
+ .HasColumnType("datetime2");
+
+ b.Property("IsActive")
+ .HasColumnType("bit");
+
+ b.Property("IsBanned")
+ .HasColumnType("bit");
+
+ b.Property("LastLoginDate")
+ .HasColumnType("datetime2");
+
+ b.Property("LastName")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("bit");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("NormalizedEmail")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("NormalizedUserName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("Notes")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PasskeyPromptDismissed")
+ .HasColumnType("bit");
+
+ b.Property("PasswordHash")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("bit");
+
+ b.Property("Position")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ProfilePictureFilePath")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("SidebarColor")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("State")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TerminationDate")
+ .HasColumnType("datetime2");
+
+ b.Property("Theme")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TimeZone")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("bit");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("UserName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("ZipCode")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CompanyId");
+
+ b.HasIndex("NormalizedEmail")
+ .HasDatabaseName("EmailIndex");
+
+ b.HasIndex("NormalizedUserName")
+ .IsUnique()
+ .HasDatabaseName("UserNameIndex")
+ .HasFilter("[NormalizedUserName] IS NOT NULL");
+
+ b.ToTable("AspNetUsers", (string)null);
+ });
+
+ modelBuilder.Entity("PowderCoating.Core.Entities.Appointment", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ActualEndTime")
+ .HasColumnType("datetime2");
+
+ b.Property("ActualStartTime")
+ .HasColumnType("datetime2");
+
+ b.Property("AppointmentNumber")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("AppointmentStatusId")
+ .HasColumnType("int");
+
+ b.Property("AppointmentTypeId")
+ .HasColumnType("int");
+
+ b.Property("AssignedUserId")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("CompanyId")
+ .HasColumnType("int");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CustomerId")
+ .HasColumnType("int");
+
+ b.Property("DeletedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("DeletedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Description")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("IsAllDay")
+ .HasColumnType("bit");
+
+ b.Property("IsDeleted")
+ .HasColumnType("bit");
+
+ b.Property("IsReminderEnabled")
+ .HasColumnType("bit");
+
+ b.Property("JobId")
+ .HasColumnType("int");
+
+ b.Property("Location")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Notes")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ReminderMinutesBefore")
+ .HasColumnType("int");
+
+ b.Property("ScheduledEndTime")
+ .HasColumnType("datetime2");
+
+ b.Property("ScheduledStartTime")
+ .HasColumnType("datetime2");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppointmentStatusId");
+
+ b.HasIndex("AppointmentTypeId");
+
+ b.HasIndex("AssignedUserId");
+
+ b.HasIndex("CustomerId");
+
+ b.HasIndex("JobId");
+
+ b.HasIndex("ScheduledStartTime");
+
+ b.HasIndex("CompanyId", "AppointmentStatusId")
+ .HasDatabaseName("IX_Appointments_CompanyId_AppointmentStatusId");
+
+ b.HasIndex("CompanyId", "ScheduledStartTime")
+ .HasDatabaseName("IX_Appointments_CompanyId_ScheduledStartTime");
+
+ b.ToTable("Appointments");
+ });
+
+ modelBuilder.Entity("PowderCoating.Core.Entities.AppointmentStatusLookup", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ColorClass")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CompanyId")
+ .HasColumnType("int");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("DeletedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("DeletedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Description")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("DisplayName")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("DisplayOrder")
+ .HasColumnType("int");
+
+ b.Property("IconClass")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("IsActive")
+ .HasColumnType("bit");
+
+ b.Property("IsDeleted")
+ .HasColumnType("bit");
+
+ b.Property("IsSystemDefined")
+ .HasColumnType("bit");
+
+ b.Property("IsTerminalStatus")
+ .HasColumnType("bit");
+
+ b.Property("StatusCode")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.ToTable("AppointmentStatusLookups");
+ });
+
+ modelBuilder.Entity("PowderCoating.Core.Entities.AppointmentTypeLookup", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ColorClass")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CompanyId")
+ .HasColumnType("int");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("DeletedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("DeletedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Description")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("DisplayName")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("DisplayOrder")
+ .HasColumnType("int");
+
+ b.Property("IconClass")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("IsActive")
+ .HasColumnType("bit");
+
+ b.Property("IsDeleted")
+ .HasColumnType("bit");
+
+ b.Property("IsSystemDefined")
+ .HasColumnType("bit");
+
+ b.Property("RequiresJobLink")
+ .HasColumnType("bit");
+
+ b.Property("TypeCode")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.ToTable("AppointmentTypeLookups");
+ });
+
+ modelBuilder.Entity("PowderCoating.Core.Entities.AuditLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Action")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CompanyId")
+ .HasColumnType("int");
+
+ b.Property("CompanyName")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property