Add invoice SMS notifications and customer intake kiosk
Invoice SMS:
- Send Invoice modal now prompts Email/SMS/Both based on customer contact data
- New /invoice/{token} customer-facing view page with full line items and pay button
- PublicViewToken (permanent) added to Invoice; separate from expiring PaymentLinkToken
- InvoiceSent SMS default template added; customizable via Notification Templates settings
- {{viewUrl}} placeholder documented in template editor
Customer Intake Kiosk:
- Tablet kiosk flow: Contact → Job → Terms/Signature → Confirmation
- Remote link mode for off-site customers (lighter form, no signature)
- KioskHub (AllowAnonymous SignalR) for staff-to-tablet push without login
- Staff activates tablet via cookie; sends remote link manually
- Submitted sessions create Customer + Job automatically; fires in-app notification
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -367,6 +367,10 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser>, IDataPro
|
||||
/// <summary>Prep-service definitions within a job template item.</summary>
|
||||
public DbSet<JobTemplateItemPrepService> JobTemplateItemPrepServices { get; set; }
|
||||
|
||||
// Customer Intake Kiosk
|
||||
/// <summary>Customer self-service intake sessions (walk-in tablet or remote email link); tenant-filtered with soft delete.</summary>
|
||||
public DbSet<KioskSession> KioskSessions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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<ApplicationUser>, IDataPro
|
||||
modelBuilder.Entity<InAppNotification>().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<KioskSession>().HasQueryFilter(e =>
|
||||
!e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
|
||||
modelBuilder.Entity<KioskSession>()
|
||||
.HasIndex(e => e.SessionToken)
|
||||
.IsUnique();
|
||||
modelBuilder.Entity<KioskSession>()
|
||||
.HasOne(k => k.LinkedCustomer)
|
||||
.WithMany()
|
||||
.HasForeignKey(k => k.LinkedCustomerId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
modelBuilder.Entity<KioskSession>()
|
||||
.HasOne(k => k.LinkedJob)
|
||||
.WithMany()
|
||||
.HasForeignKey(k => k.LinkedJobId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
// Account self-referencing hierarchy
|
||||
modelBuilder.Entity<Account>()
|
||||
.HasOne(a => a.ParentAccount)
|
||||
|
||||
Reference in New Issue
Block a user