Initial commit

This commit is contained in:
2026-04-23 21:38:24 -04:00
commit 63e12a9636
1762 changed files with 1672620 additions and 0 deletions
@@ -0,0 +1,18 @@
namespace PowderCoating.Application.Configuration;
public class StorageSettings
{
public string ConnectionString { get; set; } = string.Empty;
public StorageContainers Containers { get; set; } = new();
}
public class StorageContainers
{
public string ProfileImages { get; set; } = "profileimages";
public string JobImages { get; set; } = "jobimages";
public string Manuals { get; set; } = "manuals";
public string CompanyLogos { get; set; } = "companylogos";
public string ReceiptImages { get; set; } = "receiptimages";
public string QuoteImages { get; set; } = "quoteimages";
public string BugReportMedia { get; set; } = "bugreportmedia";
}
@@ -0,0 +1,324 @@
namespace PowderCoating.Application.DTOs.AI;
// ── Shared ────────────────────────────────────────────────────────────────────
/// <summary>Lightweight account summary passed to AI for account matching.</summary>
public class AccountSummary
{
public int Id { get; set; }
public string AccountNumber { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string AccountType { get; set; } = string.Empty; // "Expense", "CostOfGoods", "Asset"
public string? AccountSubType { get; set; }
}
// ── Feature 1: Receipt / Bill Scanning ───────────────────────────────────────
public class ScannedLineItem
{
public string Description { get; set; } = string.Empty;
public decimal Amount { get; set; }
public int? SuggestedAccountId { get; set; }
public string? SuggestedAccountName { get; set; }
}
public class ReceiptScanResult
{
public bool Success { get; set; }
public string? ErrorMessage { get; set; }
public string? VendorName { get; set; }
public string? Date { get; set; } // ISO 8601 date string
public decimal? Total { get; set; }
public string? InvoiceNumber { get; set; }
public List<ScannedLineItem> LineItems { get; set; } = new();
}
/// <summary>Internal JSON schema that Claude returns for receipt scans.</summary>
public class ClaudeReceiptResponse
{
public string? VendorName { get; set; }
public string? Date { get; set; }
public decimal? Total { get; set; }
public string? InvoiceNumber { get; set; }
public List<ClaudeReceiptLineItem> LineItems { get; set; } = new();
}
public class ClaudeReceiptLineItem
{
public string Description { get; set; } = string.Empty;
public decimal Amount { get; set; }
public int? SuggestedAccountId { get; set; }
public string? SuggestedAccountName { get; set; }
}
// ── Feature 2: AR Follow-up Email Drafts ─────────────────────────────────────
public class OverdueInvoice
{
public string InvoiceNumber { get; set; } = string.Empty;
public decimal Amount { get; set; }
public int DaysOverdue { get; set; }
}
public class ArFollowUpRequest
{
public string CustomerName { get; set; } = string.Empty;
public string CompanyName { get; set; } = string.Empty;
public decimal AmountOwed { get; set; }
public int DaysOverdue { get; set; }
public List<OverdueInvoice> Invoices { get; set; } = new();
}
public class ArFollowUpResult
{
public bool Success { get; set; }
public string? ErrorMessage { get; set; }
public string Subject { get; set; } = string.Empty;
public string Body { get; set; } = string.Empty;
}
/// <summary>Internal JSON schema that Claude returns for AR email drafts.</summary>
public class ClaudeArEmailResponse
{
public string Subject { get; set; } = string.Empty;
public string Body { get; set; } = string.Empty;
}
// ── Feature 3: Smart Account Categorization ──────────────────────────────────
public class AccountSuggestionRequest
{
public string? VendorName { get; set; }
public string? Description { get; set; }
public decimal Amount { get; set; }
public List<AccountSummary> AvailableAccounts { get; set; } = new();
}
public class AccountSuggestion
{
public int AccountId { get; set; }
public string AccountName { get; set; } = string.Empty;
public double Confidence { get; set; }
public string? Reasoning { get; set; }
}
public class AccountSuggestionResult
{
public bool Success { get; set; }
public string? ErrorMessage { get; set; }
public int? SuggestedAccountId { get; set; }
public string? SuggestedAccountName { get; set; }
public string? Reasoning { get; set; }
public List<AccountSuggestion> Alternatives { get; set; } = new();
}
/// <summary>Internal JSON schema that Claude returns for account suggestions.</summary>
public class ClaudeAccountSuggestionResponse
{
public int? SuggestedAccountId { get; set; }
public string? SuggestedAccountName { get; set; }
public string? Reasoning { get; set; }
public List<ClaudeAccountAlternative> Alternatives { get; set; } = new();
}
public class ClaudeAccountAlternative
{
public int AccountId { get; set; }
public string AccountName { get; set; } = string.Empty;
public double Confidence { get; set; }
public string? Reasoning { get; set; }
}
// ── Feature 4: Plain-English Financial Summary ────────────────────────────────
public class ExpenseByCategory
{
public string Category { get; set; } = string.Empty;
public decimal Amount { get; set; }
}
public class FinancialSummaryRequest
{
public string CompanyName { get; set; } = string.Empty;
public string Period { get; set; } = string.Empty; // e.g. "Last 6 months"
public decimal TotalRevenue { get; set; }
public decimal TotalExpenses { get; set; }
public decimal NetIncome { get; set; }
public decimal PriorMonthRevenue { get; set; }
public decimal PriorMonthExpenses { get; set; }
public decimal TotalArOutstanding { get; set; }
public decimal ArOverdue30Days { get; set; }
public int OverdueInvoiceCount { get; set; }
public List<ExpenseByCategory> ExpensesByCategory { get; set; } = new();
}
public class FinancialSummaryResult
{
public bool Success { get; set; }
public string? ErrorMessage { get; set; }
/// <summary>Markdown bullet lines (plain English, no jargon).</summary>
public List<string> Bullets { get; set; } = new();
/// <summary>"positive", "neutral", or "concerning".</summary>
public string Sentiment { get; set; } = "neutral";
}
/// <summary>Internal JSON schema that Claude returns for financial summaries.</summary>
public class ClaudeFinancialSummaryResponse
{
public List<string> Bullets { get; set; } = new();
public string Sentiment { get; set; } = "neutral";
}
// ── Feature 5: Cash Flow Forecast ─────────────────────────────────────────────
public class CashFlowArItem
{
public string CustomerName { get; set; } = string.Empty;
public string InvoiceNumber { get; set; } = string.Empty;
public decimal BalanceDue { get; set; }
public string? DueDateIso { get; set; }
public int DaysOverdue { get; set; }
public int AvgDaysToPay { get; set; }
}
public class CashFlowApItem
{
public string VendorName { get; set; } = string.Empty;
public string BillNumber { get; set; } = string.Empty;
public decimal BalanceDue { get; set; }
public string? DueDateIso { get; set; }
}
public class CashFlowJobItem
{
public string JobNumber { get; set; } = string.Empty;
public string CustomerName { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public decimal EstimatedValue { get; set; }
}
public class CashFlowForecastRequest
{
public string CompanyName { get; set; } = string.Empty;
public string AsOfDate { get; set; } = string.Empty;
public List<CashFlowArItem> OpenInvoices { get; set; } = new();
public List<CashFlowApItem> OpenBills { get; set; } = new();
public List<CashFlowJobItem> ActiveJobs { get; set; } = new();
}
public class CashFlowPeriod
{
public decimal ExpectedInflows { get; set; }
public decimal ExpectedOutflows { get; set; }
public decimal NetCashFlow { get; set; }
public List<string> KeyItems { get; set; } = new();
}
public class CashFlowForecastResult
{
public bool Success { get; set; }
public string? ErrorMessage { get; set; }
public CashFlowPeriod Next30Days { get; set; } = new();
public CashFlowPeriod Next60Days { get; set; } = new();
public CashFlowPeriod Next90Days { get; set; } = new();
public List<string> Insights { get; set; } = new();
/// <summary>"strong", "moderate", "tight", or "concerning"</summary>
public string Outlook { get; set; } = "moderate";
}
/// <summary>Internal JSON schema that Claude returns for cash flow forecasts.</summary>
public class ClaudeCashFlowResponse
{
public ClaudeCashFlowPeriod Next30Days { get; set; } = new();
public ClaudeCashFlowPeriod Next60Days { get; set; } = new();
public ClaudeCashFlowPeriod Next90Days { get; set; } = new();
public List<string> Insights { get; set; } = new();
public string Outlook { get; set; } = "moderate";
}
public class ClaudeCashFlowPeriod
{
public decimal ExpectedInflows { get; set; }
public decimal ExpectedOutflows { get; set; }
public decimal NetCashFlow { get; set; }
public List<string> KeyItems { get; set; } = new();
}
// ── Feature 6: Anomaly / Duplicate Detection ──────────────────────────────────
public class AnomalyBillSummary
{
public int Id { get; set; }
public string BillNumber { get; set; } = string.Empty;
public string VendorName { get; set; } = string.Empty;
public decimal Total { get; set; }
public string BillDateIso { get; set; } = string.Empty;
public string? VendorInvoiceNumber { get; set; }
}
public class AnomalyVendorHistory
{
public string VendorName { get; set; } = string.Empty;
public decimal AverageInvoiceAmount { get; set; }
public decimal AverageMonthlySpend { get; set; }
public int InvoiceCount { get; set; }
}
public class AnomalyAccountTrend
{
public string AccountName { get; set; } = string.Empty;
public decimal ThisMonthAmount { get; set; }
public decimal LastMonthAmount { get; set; }
public decimal AverageMonthlyAmount { get; set; }
}
public class AnomalyDetectionRequest
{
public string CompanyName { get; set; } = string.Empty;
public List<AnomalyBillSummary> RecentBills { get; set; } = new();
public List<AnomalyVendorHistory> VendorHistory { get; set; } = new();
public List<AnomalyAccountTrend> AccountTrends { get; set; } = new();
}
public class AnomalyFlag
{
/// <summary>"duplicate", "amount_spike", "unusual_vendor", "account_overrun"</summary>
public string Type { get; set; } = string.Empty;
/// <summary>"critical", "warning", "info"</summary>
public string Severity { get; set; } = "warning";
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string? RecommendedAction { get; set; }
public string? BillNumber { get; set; }
}
public class AnomalyDetectionResult
{
public bool Success { get; set; }
public string? ErrorMessage { get; set; }
public List<AnomalyFlag> Flags { get; set; } = new();
public int CriticalCount { get; set; }
public int WarningCount { get; set; }
public int InfoCount { get; set; }
}
/// <summary>Internal JSON schema that Claude returns for anomaly detection.</summary>
public class ClaudeAnomalyResponse
{
public List<ClaudeAnomalyFlag> Flags { get; set; } = new();
}
public class ClaudeAnomalyFlag
{
public string Type { get; set; } = string.Empty;
public string Severity { get; set; } = "warning";
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string? RecommendedAction { get; set; }
public string? BillNumber { get; set; }
}
@@ -0,0 +1,181 @@
namespace PowderCoating.Application.DTOs.AI;
/// <summary>Request payload sent from JS wizard to the AiAnalyzeItem endpoint.</summary>
public class AiAnalyzeItemRequest
{
/// <summary>TempIds of already-uploaded photos.</summary>
public List<string> PhotoTempIds { get; set; } = new();
// User-supplied context
public string ReferenceDimension { get; set; } = string.Empty; // e.g. "longest edge is 18 inches"
public int Quantity { get; set; } = 1;
public string DesiredColor { get; set; } = string.Empty;
public int CoatCount { get; set; } = 1;
/// <summary>Material type — helps AI factor in prep, outgassing, cure time (e.g. "Cast Iron", "Aluminum").</summary>
public string? MaterialType { get; set; }
/// <summary>Approximate weight in lbs per piece — used to factor heavy-handling surcharge into time estimate.</summary>
public decimal? EstimatedWeightLbs { get; set; }
// Follow-up support (null on first call)
public List<AiConversationTurn>? ConversationHistory { get; set; }
public string? FollowUpAnswer { get; set; }
public int CompanyId { get; set; }
/// <summary>
/// Id of the <see cref="CompanyBlastSetup"/> the user selected before analyzing.
/// When set, the controller loads that setup and passes it to the AI service so the
/// correct blast rate is injected into the prompt.
/// </summary>
public int? BlastSetupId { get; set; }
}
public class AiConversationTurn
{
public string Role { get; set; } = string.Empty; // "user" | "assistant"
public string Content { get; set; } = string.Empty;
}
/// <summary>Result returned to the JS wizard from AI analysis.</summary>
public class AiAnalyzeItemResult
{
public bool Success { get; set; }
public string? ErrorMessage { get; set; }
// When NeedsFollowUp is true, the wizard shows FollowUpQuestion to the user
public bool NeedsFollowUp { get; set; }
public string? FollowUpQuestion { get; set; }
public int FollowUpRound { get; set; } // 1 or 2
// Final item data (populated when NeedsFollowUp is false)
public string? Description { get; set; }
public decimal SurfaceAreaSqFt { get; set; }
public string Complexity { get; set; } = "Moderate"; // Simple|Moderate|Complex|Extreme
public int EstimatedMinutes { get; set; }
public string? AiReasoning { get; set; }
public string Confidence { get; set; } = "Medium"; // Low|Medium|High
// Pricing preview (filled by server using company operating costs)
public decimal EstimatedUnitPrice { get; set; }
public decimal EstimatedTotal { get; set; }
public decimal PowderCostPerLb { get; set; }
public decimal CoverageSqFtPerLb { get; set; } = 30m;
public decimal TransferEfficiency { get; set; } = 65m;
// Raw conversation to pass back for follow-up round 2
public List<AiConversationTurn> ConversationHistory { get; set; } = new();
// AI-generated standardized tags from fixed taxonomy
public List<string> Tags { get; set; } = new();
// Historical price benchmark from completed jobs
public AiBenchmarkResult? Benchmark { get; set; }
// ID of the persisted AiItemPrediction record — returned on final result (not follow-up rounds).
// The JS wizard stores this per-item so the controller can link prediction to QuoteItem/JobItem on save.
public int? AiPredictionId { get; set; }
// Pricing breakdown — helps users understand and sanity-check the estimate
public AiPricingBreakdown? Breakdown { get; set; }
}
/// <summary>
/// Display-only cost breakdown returned to the client.
/// Contains computed dollar amounts only — rates, percentages, and coverage constants are intentionally excluded.
/// </summary>
public class AiPricingBreakdown
{
public decimal SurfaceAreaSqFt { get; set; }
public decimal PowderLbsPerCoat { get; set; }
public int CoatCount { get; set; }
public decimal MaterialCost { get; set; }
public decimal ConsumablesCost { get; set; }
public int EstimatedMinutes { get; set; }
/// <summary>Non-zero when a material-type minimum floor exists for this material.</summary>
public int MaterialMinMinutes { get; set; }
/// <summary>True when the floor was actually applied (AI returned less than the floor).</summary>
public bool MinFloorApplied { get; set; }
public decimal LaborCost { get; set; }
public int OvenCycleMinutes { get; set; }
public decimal OvenCost { get; set; }
public bool RequiresPreheat { get; set; }
public int PreheatMinutes { get; set; }
public decimal PreheatCost { get; set; }
public decimal SubtotalBeforeComplexity { get; set; }
public string Complexity { get; set; } = string.Empty;
public decimal ComplexityCharge { get; set; }
public decimal SubtotalBeforeMarkup { get; set; }
public decimal MarkupAmount { get; set; }
public decimal UnitPrice { get; set; }
}
/// <summary>Request to recalculate AI item price when the user overrides sq ft, minutes, or complexity.</summary>
public class AiRecalcPriceRequest
{
public decimal SurfaceAreaSqFt { get; set; }
public int EstimatedMinutes { get; set; }
public string Complexity { get; set; } = "Moderate";
public int CoatCount { get; set; } = 1;
}
/// <summary>Result of a price recalculation — returns the new unit price and a display-safe breakdown.</summary>
public class AiRecalcPriceResult
{
public bool Success { get; set; }
public decimal UnitPrice { get; set; }
public AiPricingBreakdown? Breakdown { get; set; }
}
/// <summary>Historical price benchmark from completed jobs with similar complexity and size.</summary>
public class AiBenchmarkResult
{
public int MatchCount { get; set; }
public decimal MinPrice { get; set; }
public decimal MaxPrice { get; set; }
public decimal AvgPrice { get; set; }
public string ComplexityLevel { get; set; } = string.Empty;
public decimal SqFtRangeMin { get; set; }
public decimal SqFtRangeMax { get; set; }
}
/// <summary>
/// Company-specific AI context passed to the quote service on every analysis call.
/// Contains optional free-text profile and recent accepted predictions as few-shot calibration examples.
/// </summary>
public class CompanyAiContext
{
/// <summary>Free-text description of the shop's specialties and pricing style (from Company Settings).</summary>
public string? ProfileText { get; set; }
/// <summary>Recent predictions the user accepted without override — used as few-shot calibration examples.</summary>
public List<AiFewShotExample> AcceptedExamples { get; set; } = new();
}
/// <summary>A single accepted-prediction record formatted for AI few-shot context.</summary>
public class AiFewShotExample
{
public string Description { get; set; } = string.Empty;
public decimal SurfaceAreaSqFt { get; set; }
public string Complexity { get; set; } = string.Empty;
public int EstimatedMinutes { get; set; }
public decimal FinalUnitPrice { get; set; }
public string? Tags { get; set; }
}
/// <summary>Internal structured response that Claude returns as JSON.</summary>
public class ClaudeQuoteResponse
{
public string Description { get; set; } = string.Empty;
public decimal SurfaceAreaSqFt { get; set; }
public string Complexity { get; set; } = "Moderate";
public int EstimatedMinutes { get; set; }
public bool RequiresPreheat { get; set; }
public int PreheatMinutes { get; set; }
public string Confidence { get; set; } = "Medium";
public bool NeedsFollowUp { get; set; }
public string? FollowUpQuestion { get; set; }
public string Reasoning { get; set; } = string.Empty;
public List<string> Tags { get; set; } = new();
}
@@ -0,0 +1,90 @@
using PowderCoating.Core.Enums;
using System.ComponentModel.DataAnnotations;
namespace PowderCoating.Application.DTOs.Accounting;
public class AccountListDto
{
public int Id { get; set; }
public string AccountNumber { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public AccountType AccountType { get; set; }
public AccountSubType AccountSubType { get; set; }
public bool IsSystem { get; set; }
public bool IsActive { get; set; }
public int? ParentAccountId { get; set; }
public string? ParentAccountName { get; set; }
public decimal OpeningBalance { get; set; }
public DateTime? OpeningBalanceDate { get; set; }
public decimal CurrentBalance { get; set; }
}
public class AccountDto : AccountListDto
{
public string? Description { get; set; }
public DateTime CreatedAt { get; set; }
public List<AccountListDto> SubAccounts { get; set; } = new();
}
public class CreateAccountDto
{
[Required, MaxLength(20)]
public string AccountNumber { get; set; } = string.Empty;
[Required, MaxLength(150)]
public string Name { get; set; } = string.Empty;
[Required]
public AccountType AccountType { get; set; }
[Required]
public AccountSubType AccountSubType { get; set; }
public string? Description { get; set; }
public int? ParentAccountId { get; set; }
public bool IsActive { get; set; } = true;
[Display(Name = "Opening Balance")]
public decimal OpeningBalance { get; set; } = 0;
[Display(Name = "Opening Balance Date")]
[DataType(DataType.Date)]
public DateTime? OpeningBalanceDate { get; set; }
}
public class EditAccountDto : CreateAccountDto
{
public int Id { get; set; }
}
public class LedgerEntryDto
{
public DateTime Date { get; set; }
public string Reference { get; set; } = string.Empty;
public string Source { get; set; } = string.Empty;
public string? Description { get; set; }
public decimal Debit { get; set; }
public decimal Credit { get; set; }
public decimal RunningBalance { get; set; }
public string? LinkController { get; set; }
public int? LinkId { get; set; }
}
public class AccountLedgerDto
{
public int Id { get; set; }
public string AccountNumber { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public AccountType AccountType { get; set; }
public AccountSubType AccountSubType { get; set; }
public DateTime From { get; set; }
public DateTime To { get; set; }
public decimal OpeningBalance { get; set; } // Balance at start of the requested period
public decimal PeriodDebits { get; set; }
public decimal PeriodCredits { get; set; }
public decimal ClosingBalance { get; set; }
public List<LedgerEntryDto> Entries { get; set; } = new();
}
@@ -0,0 +1,186 @@
using PowderCoating.Core.Enums;
using System.ComponentModel.DataAnnotations;
namespace PowderCoating.Application.DTOs.Accounting;
/// <summary>Unified row for the combined Bills / Expenses list view.</summary>
public class BillExpenseListDto
{
public string EntryType { get; set; } = "Bill"; // "Bill" | "Expense"
public int Id { get; set; }
public string Number { get; set; } = string.Empty;
public DateTime Date { get; set; }
public string? VendorName { get; set; }
public string? Memo { get; set; }
public string? AccountName { get; set; }
public decimal Total { get; set; }
public decimal BalanceDue { get; set; }
public string StatusLabel { get; set; } = string.Empty;
public string StatusColor { get; set; } = "secondary";
public DateTime? DueDate { get; set; }
public bool IsOverdue { get; set; }
public bool HasReceipt { get; set; }
}
public class BillListDto
{
public int Id { get; set; }
public string BillNumber { get; set; } = string.Empty;
public string? VendorInvoiceNumber { get; set; }
public int VendorId { get; set; }
public string VendorName { get; set; } = string.Empty;
public BillStatus Status { get; set; }
public DateTime BillDate { get; set; }
public DateTime? DueDate { get; set; }
public decimal Total { get; set; }
public decimal AmountPaid { get; set; }
public decimal BalanceDue { get; set; }
public bool IsOverdue => Status != BillStatus.Paid && Status != BillStatus.Voided
&& DueDate.HasValue && DueDate.Value.Date < DateTime.Today;
}
public class BillDto
{
public int Id { get; set; }
public string BillNumber { get; set; } = string.Empty;
public string? VendorInvoiceNumber { get; set; }
public int VendorId { get; set; }
public string VendorName { get; set; } = string.Empty;
public string? VendorEmail { get; set; }
public string? VendorPhone { get; set; }
public int APAccountId { get; set; }
public string APAccountName { get; set; } = string.Empty;
public BillStatus Status { get; set; }
public DateTime BillDate { get; set; }
public DateTime? DueDate { get; set; }
public string? Terms { get; set; }
public string? Memo { get; set; }
public decimal SubTotal { get; set; }
public decimal TaxPercent { get; set; }
public decimal TaxAmount { get; set; }
public decimal Total { get; set; }
public decimal AmountPaid { get; set; }
public decimal BalanceDue { get; set; }
public string? ReceiptFilePath { get; set; }
public List<BillLineItemDto> LineItems { get; set; } = new();
public List<BillPaymentDto> Payments { get; set; } = new();
}
public class BillLineItemDto
{
public int Id { get; set; }
public int? AccountId { get; set; }
public string AccountName { get; set; } = string.Empty;
public string AccountNumber { get; set; } = string.Empty;
public int? JobId { get; set; }
public string? JobNumber { get; set; }
public string Description { get; set; } = string.Empty;
public decimal Quantity { get; set; }
public decimal UnitPrice { get; set; }
public decimal Amount { get; set; }
public int DisplayOrder { get; set; }
}
public class BillPaymentDto
{
public int Id { get; set; }
public string PaymentNumber { get; set; } = string.Empty;
public DateTime PaymentDate { get; set; }
public decimal Amount { get; set; }
public PaymentMethod PaymentMethod { get; set; }
public string? CheckNumber { get; set; }
public string? Memo { get; set; }
public int BankAccountId { get; set; }
public string BankAccountName { get; set; } = string.Empty;
}
public class CreateBillDto
{
[Required]
public int VendorId { get; set; }
public int APAccountId { get; set; }
[MaxLength(100)]
public string? VendorInvoiceNumber { get; set; }
[Required]
public DateTime BillDate { get; set; } = DateTime.Today;
public DateTime? DueDate { get; set; }
public string? Terms { get; set; }
public string? Memo { get; set; }
public decimal TaxPercent { get; set; }
/// <summary>When set, the created bill is linked back to this PO.</summary>
public int? PurchaseOrderId { get; set; }
public List<CreateBillLineItemDto> LineItems { get; set; } = new();
}
public class CreateBillLineItemDto
{
public int? AccountId { get; set; }
public int? JobId { get; set; }
[Required, MaxLength(500)]
public string Description { get; set; } = string.Empty;
[Required]
public decimal Quantity { get; set; } = 1;
[Required]
public decimal UnitPrice { get; set; }
public int DisplayOrder { get; set; }
}
public class EditBillDto : CreateBillDto
{
public int Id { get; set; }
public string? ReceiptFilePath { get; set; }
}
public class RecordBillPaymentDto
{
public int BillId { get; set; }
[Required]
public DateTime PaymentDate { get; set; } = DateTime.Today;
[Required, Range(0.01, double.MaxValue, ErrorMessage = "Amount must be greater than zero")]
public decimal Amount { get; set; }
[Required]
public PaymentMethod PaymentMethod { get; set; }
[Required]
public int BankAccountId { get; set; }
[MaxLength(50)]
public string? CheckNumber { get; set; }
[MaxLength(500)]
public string? Memo { get; set; }
}
public class EditBillPaymentDto
{
public int PaymentId { get; set; }
public int BillId { get; set; }
[Required]
public DateTime PaymentDate { get; set; }
[Required]
public PaymentMethod PaymentMethod { get; set; }
[Required]
public int BankAccountId { get; set; }
[MaxLength(50)]
public string? CheckNumber { get; set; }
[MaxLength(500)]
public string? Memo { get; set; }
}
@@ -0,0 +1,62 @@
using PowderCoating.Core.Enums;
using System.ComponentModel.DataAnnotations;
namespace PowderCoating.Application.DTOs.Accounting;
public class ExpenseListDto
{
public int Id { get; set; }
public string ExpenseNumber { get; set; } = string.Empty;
public DateTime Date { get; set; }
public int? VendorId { get; set; }
public string? VendorName { get; set; }
public int ExpenseAccountId { get; set; }
public string ExpenseAccountName { get; set; } = string.Empty;
public string ExpenseAccountNumber { get; set; } = string.Empty;
public int PaymentAccountId { get; set; }
public string PaymentAccountName { get; set; } = string.Empty;
public PaymentMethod PaymentMethod { get; set; }
public decimal Amount { get; set; }
public string? Memo { get; set; }
public int? JobId { get; set; }
public string? JobNumber { get; set; }
public bool HasReceipt { get; set; }
}
public class ExpenseDto : ExpenseListDto
{
public string? ReceiptFilePath { get; set; }
public DateTime CreatedAt { get; set; }
}
public class CreateExpenseDto
{
[Required]
public DateTime Date { get; set; } = DateTime.Today;
public int? VendorId { get; set; }
[Required]
public int ExpenseAccountId { get; set; }
[Required]
public int PaymentAccountId { get; set; }
public int? JobId { get; set; }
[Required]
public PaymentMethod PaymentMethod { get; set; }
[Required, Range(0.01, double.MaxValue, ErrorMessage = "Amount must be greater than zero")]
public decimal Amount { get; set; }
[MaxLength(500)]
public string? Memo { get; set; }
public string? ReceiptFilePath { get; set; }
}
public class EditExpenseDto : CreateExpenseDto
{
public int Id { get; set; }
}
@@ -0,0 +1,161 @@
using PowderCoating.Core.Enums;
namespace PowderCoating.Application.DTOs.Accounting;
// ── Profit & Loss ─────────────────────────────────────────────────────────────
public class ProfitAndLossDto
{
public DateTime From { get; set; }
public DateTime To { get; set; }
public string CompanyName { get; set; } = string.Empty;
public List<FinancialReportLine> RevenueLines { get; set; } = new();
public decimal TotalRevenue { get; set; }
public List<FinancialReportLine> CogsLines { get; set; } = new();
public decimal TotalCogs { get; set; }
public decimal GrossProfit => TotalRevenue - TotalCogs;
public decimal GrossMarginPercent => TotalRevenue == 0 ? 0 : Math.Round(GrossProfit / TotalRevenue * 100, 1);
public List<FinancialReportLine> ExpenseLines { get; set; } = new();
public decimal TotalExpenses { get; set; }
public decimal OperatingIncome => GrossProfit - TotalExpenses;
public decimal NetIncome => OperatingIncome; // Extend later for other income/tax
}
public class FinancialReportLine
{
public int AccountId { get; set; }
public string AccountNumber { get; set; } = string.Empty;
public string AccountName { get; set; } = string.Empty;
public decimal Amount { get; set; }
}
// ── Balance Sheet ─────────────────────────────────────────────────────────────
public class BalanceSheetDto
{
public DateTime AsOf { get; set; }
public string CompanyName { get; set; } = string.Empty;
// Assets
public List<FinancialReportLine> CurrentAssets { get; set; } = new();
public List<FinancialReportLine> FixedAssets { get; set; } = new();
public List<FinancialReportLine> OtherAssets { get; set; } = new();
public decimal TotalAssets { get; set; }
// Liabilities
public List<FinancialReportLine> CurrentLiabilities { get; set; } = new();
public List<FinancialReportLine> LongTermLiabilities { get; set; } = new();
public decimal TotalLiabilities { get; set; }
// Equity
public List<FinancialReportLine> EquityLines { get; set; } = new();
public decimal RetainedEarnings { get; set; } // Computed net income to date
public decimal TotalEquity { get; set; }
public decimal TotalLiabilitiesAndEquity => TotalLiabilities + TotalEquity;
// Helper: is the sheet balanced?
public bool IsBalanced => Math.Abs(TotalAssets - TotalLiabilitiesAndEquity) < 0.01m;
}
// ── AR Aging ──────────────────────────────────────────────────────────────────
public class ArAgingReportDto
{
public DateTime AsOf { get; set; }
public string CompanyName { get; set; } = string.Empty;
public List<ArAgingCustomerDto> Customers { get; set; } = new();
public decimal TotalCurrent { get; set; }
public decimal Total1to30 { get; set; }
public decimal Total31to60 { get; set; }
public decimal Total61to90 { get; set; }
public decimal TotalOver90 { get; set; }
public decimal TotalOutstanding => TotalCurrent + Total1to30 + Total31to60 + Total61to90 + TotalOver90;
}
public class ArAgingCustomerDto
{
public int CustomerId { get; set; }
public string CustomerName { get; set; } = string.Empty;
public List<ArAgingInvoiceDto> Invoices { get; set; } = new();
public decimal TotalCurrent { get; set; }
public decimal Total1to30 { get; set; }
public decimal Total31to60 { get; set; }
public decimal Total61to90 { get; set; }
public decimal TotalOver90 { get; set; }
public decimal TotalBalance => TotalCurrent + Total1to30 + Total31to60 + Total61to90 + TotalOver90;
}
public class ArAgingInvoiceDto
{
public int InvoiceId { get; set; }
public string InvoiceNumber { get; set; } = string.Empty;
public DateTime InvoiceDate { get; set; }
public DateTime? DueDate { get; set; }
public decimal BalanceDue { get; set; }
public int DaysOverdue { get; set; }
}
// ── Sales & Income ────────────────────────────────────────────────────────────
public class SalesIncomeReportDto
{
public DateTime From { get; set; }
public DateTime To { get; set; }
public string CompanyName { get; set; } = string.Empty;
public decimal TotalInvoiced { get; set; }
public decimal TotalCollected { get; set; }
public decimal TotalTax { get; set; }
public decimal TotalDiscount { get; set; }
public decimal NetRevenue => TotalInvoiced - TotalDiscount;
public int InvoiceCount { get; set; }
public int CustomerCount { get; set; }
public decimal AverageInvoiceValue => InvoiceCount == 0 ? 0 : Math.Round(TotalInvoiced / InvoiceCount, 2);
public List<SalesByCustomerDto> ByCustomer { get; set; } = new();
public List<SalesByMonthDto> ByMonth { get; set; } = new();
public List<SalesInvoiceLineDto> Invoices { get; set; } = new();
}
public class SalesByCustomerDto
{
public int CustomerId { get; set; }
public string CustomerName { get; set; } = string.Empty;
public int InvoiceCount { get; set; }
public decimal TotalInvoiced { get; set; }
public decimal TotalPaid { get; set; }
public decimal BalanceDue { get; set; }
}
public class SalesByMonthDto
{
public int Year { get; set; }
public int Month { get; set; }
public string Label { get; set; } = string.Empty;
public decimal TotalInvoiced { get; set; }
public decimal TotalCollected { get; set; }
public int InvoiceCount { get; set; }
}
public class SalesInvoiceLineDto
{
public int InvoiceId { get; set; }
public string InvoiceNumber { get; set; } = string.Empty;
public string CustomerName { get; set; } = string.Empty;
public DateTime InvoiceDate { get; set; }
public DateTime? DueDate { get; set; }
public string Status { get; set; } = string.Empty;
public decimal SubTotal { get; set; }
public decimal TaxAmount { get; set; }
public decimal Total { get; set; }
public decimal AmountPaid { get; set; }
public decimal BalanceDue { get; set; }
}
@@ -0,0 +1,216 @@
using System.ComponentModel.DataAnnotations;
namespace PowderCoating.Application.DTOs.Appointment;
/// <summary>
/// Full appointment details DTO for Details view
/// </summary>
public class AppointmentDto
{
public int Id { get; set; }
public string AppointmentNumber { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string? Description { get; set; }
// Customer information (optional for internal appointments)
public int? CustomerId { get; set; }
public string? CustomerName { get; set; }
public string? CustomerPhone { get; set; }
public string? CustomerEmail { get; set; }
// Job link (optional)
public int? JobId { get; set; }
public string? JobNumber { get; set; }
// Appointment Type (from lookup table)
public int AppointmentTypeId { get; set; }
public string TypeCode { get; set; } = string.Empty;
public string TypeDisplayName { get; set; } = string.Empty;
public string TypeColorClass { get; set; } = "primary";
public string? TypeIconClass { get; set; }
public bool TypeRequiresJobLink { get; set; }
// Appointment Status (from lookup table)
public int AppointmentStatusId { get; set; }
public string StatusCode { get; set; } = string.Empty;
public string StatusDisplayName { get; set; } = string.Empty;
public string StatusColorClass { get; set; } = "secondary";
public string? StatusIconClass { get; set; }
public bool StatusIsTerminal { get; set; }
// Timing
public DateTime ScheduledStartTime { get; set; }
public DateTime ScheduledEndTime { get; set; }
public bool IsAllDay { get; set; }
public DateTime? ActualStartTime { get; set; }
public DateTime? ActualEndTime { get; set; }
// Assignment
public string? AssignedUserId { get; set; }
public string? AssignedWorkerName { get; set; }
// Additional information
public string? Location { get; set; }
public string? Notes { get; set; }
public bool IsReminderEnabled { get; set; }
public int ReminderMinutesBefore { get; set; }
// Audit fields
public DateTime CreatedAt { get; set; }
public string? CreatedBy { get; set; }
}
/// <summary>
/// Lightweight DTO for list/grid views with pagination
/// </summary>
public class AppointmentListDto
{
public int Id { get; set; }
public string AppointmentNumber { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string? CustomerName { get; set; }
// Appointment Type
public string TypeDisplayName { get; set; } = string.Empty;
public string TypeColorClass { get; set; } = "primary";
// Appointment Status
public string StatusDisplayName { get; set; } = string.Empty;
public string StatusColorClass { get; set; } = "secondary";
// Timing
public DateTime ScheduledStartTime { get; set; }
public DateTime ScheduledEndTime { get; set; }
public bool IsAllDay { get; set; }
// Assignment
public string? AssignedWorkerName { get; set; }
public DateTime CreatedAt { get; set; }
}
/// <summary>
/// DTO for calendar event JSON API (supports both Appointments and Maintenance)
/// </summary>
public class CalendarEventDto
{
public int Id { get; set; }
public string EventType { get; set; } = "Appointment"; // "Appointment" or "Maintenance"
public string Title { get; set; } = string.Empty;
public string Start { get; set; } = string.Empty; // ISO 8601 format
public string End { get; set; } = string.Empty; // ISO 8601 format
public bool AllDay { get; set; }
public string BackgroundColor { get; set; } = "#007bff";
public string BorderColor { get; set; } = "#007bff";
public string TextColor { get; set; } = "#ffffff";
public string CustomerName { get; set; } = string.Empty; // For appointments; equipment name for maintenance
public string TypeDisplayName { get; set; } = string.Empty;
public string StatusCode { get; set; } = string.Empty;
public string? Location { get; set; }
public string JobNumber { get; set; } = string.Empty;
public bool IsFallbackDate { get; set; } // true when showing on DueDate because ScheduledDate is null
}
/// <summary>
/// DTO for creating new appointments
/// </summary>
public class CreateAppointmentDto
{
[Required]
public string Title { get; set; } = string.Empty;
public string? Description { get; set; }
public int? CustomerId { get; set; }
public int? JobId { get; set; }
[Required]
public int AppointmentTypeId { get; set; }
public string? AssignedUserId { get; set; }
[Required]
public DateTime ScheduledStartTime { get; set; }
[Required]
public DateTime ScheduledEndTime { get; set; }
public bool IsAllDay { get; set; }
public string? Location { get; set; }
public string? Notes { get; set; }
public bool IsReminderEnabled { get; set; } = true;
public int ReminderMinutesBefore { get; set; } = 30;
}
/// <summary>
/// DTO for updating existing appointments
/// </summary>
public class UpdateAppointmentDto
{
[Required]
public int Id { get; set; }
[Required]
public string Title { get; set; } = string.Empty;
public string? Description { get; set; }
public int? CustomerId { get; set; }
public int? JobId { get; set; }
[Required]
public int AppointmentStatusId { get; set; }
[Required]
public int AppointmentTypeId { get; set; }
public string? AssignedUserId { get; set; }
[Required]
public DateTime ScheduledStartTime { get; set; }
[Required]
public DateTime ScheduledEndTime { get; set; }
public bool IsAllDay { get; set; }
public DateTime? ActualStartTime { get; set; }
public DateTime? ActualEndTime { get; set; }
public string? Location { get; set; }
public string? Notes { get; set; }
public bool IsReminderEnabled { get; set; }
public int ReminderMinutesBefore { get; set; }
}
/// <summary>
/// Simplified DTO for quick creation from calendar view
/// </summary>
public class QuickCreateAppointmentDto
{
[Required]
public string Title { get; set; } = string.Empty;
public int? CustomerId { get; set; }
[Required]
public int AppointmentTypeId { get; set; }
[Required]
public DateTime ScheduledStartTime { get; set; }
[Required]
public DateTime ScheduledEndTime { get; set; }
public bool IsAllDay { get; set; }
}
@@ -0,0 +1,11 @@
namespace PowderCoating.Application.DTOs.BugReport;
public class BugReportAttachmentDto
{
public int Id { get; set; }
public int BugReportId { get; set; }
public string FileName { get; set; } = string.Empty;
public string ContentType { get; set; } = string.Empty;
public long FileSizeBytes { get; set; }
public DateTime CreatedAt { get; set; }
}
@@ -0,0 +1,22 @@
using PowderCoating.Core.Enums;
namespace PowderCoating.Application.DTOs.BugReport;
public class BugReportDto
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string SubmittedByUserId { get; set; } = string.Empty;
public string SubmittedByUserName { get; set; } = string.Empty;
public string? CompanyName { get; set; }
public BugReportPriority Priority { get; set; }
public BugReportStatus Status { get; set; }
public string? ResolutionNotes { get; set; }
public DateTime? ResolvedAt { get; set; }
public string? ResolvedBy { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public int CompanyId { get; set; }
public List<BugReportAttachmentDto> Attachments { get; set; } = new();
}
@@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;
using PowderCoating.Core.Enums;
namespace PowderCoating.Application.DTOs.BugReport;
public class CreateBugReportDto
{
[Required(ErrorMessage = "Please provide a brief title for the bug.")]
[StringLength(200, ErrorMessage = "Title cannot exceed 200 characters.")]
public string Title { get; set; } = string.Empty;
[Required(ErrorMessage = "Please describe the issue.")]
[StringLength(4000, ErrorMessage = "Description cannot exceed 4000 characters.")]
public string Description { get; set; } = string.Empty;
public BugReportPriority Priority { get; set; } = BugReportPriority.Normal;
}
@@ -0,0 +1,29 @@
using System.ComponentModel.DataAnnotations;
using PowderCoating.Core.Enums;
namespace PowderCoating.Application.DTOs.BugReport;
public class EditBugReportDto
{
public int Id { get; set; }
[Required(ErrorMessage = "Title is required.")]
[StringLength(200, ErrorMessage = "Title cannot exceed 200 characters.")]
public string Title { get; set; } = string.Empty;
[Required(ErrorMessage = "Description is required.")]
[StringLength(4000, ErrorMessage = "Description cannot exceed 4000 characters.")]
public string Description { get; set; } = string.Empty;
public string SubmittedByUserName { get; set; } = string.Empty;
public string? CompanyName { get; set; }
public DateTime CreatedAt { get; set; }
public int CompanyId { get; set; }
public List<BugReportAttachmentDto> Attachments { get; set; } = new();
public BugReportPriority Priority { get; set; }
public BugReportStatus Status { get; set; }
[StringLength(4000, ErrorMessage = "Resolution notes cannot exceed 4000 characters.")]
public string? ResolutionNotes { get; set; }
}
@@ -0,0 +1,164 @@
using System.ComponentModel.DataAnnotations;
namespace PowderCoating.Application.DTOs.Catalog
{
/// <summary>
/// DTO for displaying detailed catalog item information.
/// </summary>
public class CatalogItemDto
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public string? SKU { get; set; }
public int CategoryId { get; set; }
public string CategoryName { get; set; } = string.Empty;
public string FullCategoryPath { get; set; } = string.Empty;
public decimal DefaultPrice { get; set; }
public bool DefaultRequiresSandblasting { get; set; }
public bool DefaultRequiresMasking { get; set; }
public int? DefaultEstimatedMinutes { get; set; }
public decimal? ApproximateArea { get; set; }
public int DisplayOrder { get; set; }
public bool IsActive { get; set; }
[Display(Name = "Revenue Account")]
public int? RevenueAccountId { get; set; }
public string? RevenueAccountName { get; set; }
[Display(Name = "COGS Account")]
public int? CogsAccountId { get; set; }
public string? CogsAccountName { get; set; }
}
/// <summary>
/// DTO for displaying catalog items in list/grid views.
/// Contains only essential fields for performance.
/// </summary>
public class CatalogItemListDto
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? SKU { get; set; }
public string CategoryName { get; set; } = string.Empty;
public decimal DefaultPrice { get; set; }
public bool IsActive { get; set; }
}
/// <summary>
/// DTO for creating a new catalog item.
/// </summary>
public class CreateCatalogItemDto
{
[Required(ErrorMessage = "Item name is required")]
[StringLength(200, ErrorMessage = "Item name cannot exceed 200 characters")]
[Display(Name = "Item Name")]
public string Name { get; set; } = string.Empty;
[StringLength(1000, ErrorMessage = "Description cannot exceed 1000 characters")]
[Display(Name = "Description")]
public string? Description { get; set; }
[StringLength(50, ErrorMessage = "SKU cannot exceed 50 characters")]
[Display(Name = "SKU / Item Code")]
public string? SKU { get; set; }
[Required(ErrorMessage = "Category is required")]
[Display(Name = "Category")]
public int CategoryId { get; set; }
[Required(ErrorMessage = "Default price is required")]
[Range(0, 100000, ErrorMessage = "Price must be between $0 and $100,000")]
[Display(Name = "Default Price")]
[DataType(DataType.Currency)]
public decimal DefaultPrice { get; set; }
[Display(Name = "Requires Sandblasting")]
public bool DefaultRequiresSandblasting { get; set; }
[Display(Name = "Requires Masking/Taping")]
public bool DefaultRequiresMasking { get; set; }
[Range(1, 10000, ErrorMessage = "Estimated minutes must be between 1 and 10,000")]
[Display(Name = "Estimated Minutes")]
public int? DefaultEstimatedMinutes { get; set; }
[Range(0.01, 100000, ErrorMessage = "Area must be between 0.01 and 100,000")]
[Display(Name = "Approximate Area")]
public decimal? ApproximateArea { get; set; }
[Display(Name = "Display Order")]
[Range(0, 9999, ErrorMessage = "Display order must be between 0 and 9999")]
public int DisplayOrder { get; set; }
[Display(Name = "Revenue Account")]
public int? RevenueAccountId { get; set; }
[Display(Name = "COGS Account")]
public int? CogsAccountId { get; set; }
[Display(Name = "Available for direct sale (merchandise)")]
public bool IsMerchandise { get; set; }
}
/// <summary>
/// DTO for updating an existing catalog item.
/// </summary>
public class UpdateCatalogItemDto
{
public int Id { get; set; }
[Required(ErrorMessage = "Item name is required")]
[StringLength(200, ErrorMessage = "Item name cannot exceed 200 characters")]
[Display(Name = "Item Name")]
public string Name { get; set; } = string.Empty;
[StringLength(1000, ErrorMessage = "Description cannot exceed 1000 characters")]
[Display(Name = "Description")]
public string? Description { get; set; }
[StringLength(50, ErrorMessage = "SKU cannot exceed 50 characters")]
[Display(Name = "SKU / Item Code")]
public string? SKU { get; set; }
[Required(ErrorMessage = "Category is required")]
[Display(Name = "Category")]
public int CategoryId { get; set; }
[Required(ErrorMessage = "Default price is required")]
[Range(0, 100000, ErrorMessage = "Price must be between $0 and $100,000")]
[Display(Name = "Default Price")]
[DataType(DataType.Currency)]
public decimal DefaultPrice { get; set; }
[Display(Name = "Requires Sandblasting")]
public bool DefaultRequiresSandblasting { get; set; }
[Display(Name = "Requires Masking/Taping")]
public bool DefaultRequiresMasking { get; set; }
[Range(1, 10000, ErrorMessage = "Estimated minutes must be between 1 and 10,000")]
[Display(Name = "Estimated Minutes")]
public int? DefaultEstimatedMinutes { get; set; }
[Range(0.01, 100000, ErrorMessage = "Area must be between 0.01 and 100,000")]
[Display(Name = "Approximate Area")]
public decimal? ApproximateArea { get; set; }
[Display(Name = "Display Order")]
[Range(0, 9999, ErrorMessage = "Display order must be between 0 and 9999")]
public int DisplayOrder { get; set; }
[Display(Name = "Is Active")]
public bool IsActive { get; set; }
[Display(Name = "Revenue Account")]
public int? RevenueAccountId { get; set; }
[Display(Name = "COGS Account")]
public int? CogsAccountId { get; set; }
[Display(Name = "Available for direct sale (merchandise)")]
public bool IsMerchandise { get; set; }
}
}
@@ -0,0 +1,83 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace PowderCoating.Application.DTOs.Catalog
{
/// <summary>
/// DTO for displaying catalog category information.
/// </summary>
public class CategoryDto
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public int? ParentCategoryId { get; set; }
public string? ParentCategoryName { get; set; }
public int DisplayOrder { get; set; }
public bool IsActive { get; set; }
public int ItemCount { get; set; }
public int SubCategoryCount { get; set; }
}
/// <summary>
/// DTO for representing category hierarchy as a tree structure.
/// Used for tree views and navigation.
/// </summary>
public class CategoryTreeDto
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public int ItemCount { get; set; }
public List<CategoryTreeDto> Children { get; set; } = new();
public bool HasItems { get; set; }
}
/// <summary>
/// DTO for creating a new catalog category.
/// </summary>
public class CreateCategoryDto
{
[Required(ErrorMessage = "Category name is required")]
[StringLength(100, ErrorMessage = "Category name cannot exceed 100 characters")]
[Display(Name = "Category Name")]
public string Name { get; set; } = string.Empty;
[StringLength(500, ErrorMessage = "Description cannot exceed 500 characters")]
[Display(Name = "Description")]
public string? Description { get; set; }
[Display(Name = "Parent Category")]
public int? ParentCategoryId { get; set; }
[Display(Name = "Display Order")]
[Range(0, 9999, ErrorMessage = "Display order must be between 0 and 9999")]
public int DisplayOrder { get; set; }
}
/// <summary>
/// DTO for updating an existing catalog category.
/// </summary>
public class UpdateCategoryDto
{
public int Id { get; set; }
[Required(ErrorMessage = "Category name is required")]
[StringLength(100, ErrorMessage = "Category name cannot exceed 100 characters")]
[Display(Name = "Category Name")]
public string Name { get; set; } = string.Empty;
[StringLength(500, ErrorMessage = "Description cannot exceed 500 characters")]
[Display(Name = "Description")]
public string? Description { get; set; }
[Display(Name = "Parent Category")]
public int? ParentCategoryId { get; set; }
[Display(Name = "Display Order")]
[Range(0, 9999, ErrorMessage = "Display order must be between 0 and 9999")]
public int DisplayOrder { get; set; }
[Display(Name = "Is Active")]
public bool IsActive { get; set; }
}
}
@@ -0,0 +1,18 @@
namespace PowderCoating.Application.DTOs.Common;
public class GridRequest
{
public int PageNumber { get; set; } = 1;
public int PageSize { get; set; } = 25;
public string? SortColumn { get; set; }
public string SortDirection { get; set; } = "asc";
public string? SearchTerm { get; set; }
public void Validate()
{
if (PageNumber < 1) PageNumber = 1;
if (PageSize < 5) PageSize = 5;
if (PageSize > 100) PageSize = 100;
if (SortDirection?.ToLower() != "desc") SortDirection = "asc";
}
}
@@ -0,0 +1,17 @@
namespace PowderCoating.Application.DTOs.Common;
public class PagedResult<T>
{
public IEnumerable<T> Items { get; set; } = new List<T>();
public int PageNumber { get; set; }
public int PageSize { get; set; }
public int TotalCount { get; set; }
public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize);
public bool HasPreviousPage => PageNumber > 1;
public bool HasNextPage => PageNumber < TotalPages;
public int StartIndex => (PageNumber - 1) * PageSize + 1;
public int EndIndex => Math.Min(PageNumber * PageSize, TotalCount);
public string? SortColumn { get; set; }
public string SortDirection { get; set; } = "asc";
public string? SearchTerm { get; set; }
}
@@ -0,0 +1,228 @@
using System.ComponentModel.DataAnnotations;
using PowderCoating.Application.DTOs.User;
using PowderCoating.Core.Enums;
namespace PowderCoating.Application.DTOs.Company;
/// <summary>
/// Full company details DTO
/// </summary>
public class CompanyDto
{
public int Id { get; set; }
public string CompanyName { get; set; } = string.Empty;
public string? CompanyCode { get; set; }
public string PrimaryContactName { get; set; } = string.Empty;
public string PrimaryContactEmail { get; set; } = string.Empty;
public string? Phone { get; set; }
public string? Address { get; set; }
public string? City { get; set; }
public string? State { get; set; }
public string? ZipCode { get; set; }
public bool IsActive { get; set; }
public DateTime SubscriptionStartDate { get; set; }
public DateTime? SubscriptionEndDate { get; set; }
public int SubscriptionPlan { get; set; }
public SubscriptionStatus SubscriptionStatus { get; set; }
public string? StripeCustomerId { get; set; }
public string? StripeSubscriptionId { get; set; }
public string? TimeZone { get; set; }
public string? LogoPath { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public string? CreatedBy { get; set; }
public string? UpdatedBy { get; set; }
// Statistics
public int UserCount { get; set; }
public int CustomerCount { get; set; }
public int JobCount { get; set; }
// Users list for management
public List<CompanyUserDto> Users { get; set; } = new List<CompanyUserDto>();
}
/// <summary>
/// Company list item DTO for grid/list views
/// </summary>
public class CompanyListDto
{
public int Id { get; set; }
public string CompanyName { get; set; } = string.Empty;
public string? CompanyCode { get; set; }
public string PrimaryContactEmail { get; set; } = string.Empty;
public string? Phone { get; set; }
public int SubscriptionPlan { get; set; }
public SubscriptionStatus SubscriptionStatus { get; set; }
public bool IsActive { get; set; }
public DateTime SubscriptionStartDate { get; set; }
public DateTime? SubscriptionEndDate { get; set; }
public int UserCount { get; set; }
public int JobCount { get; set; }
public int QuoteCount { get; set; }
public int CustomerCount { get; set; }
public DateTime CreatedAt { get; set; }
public bool WizardCompleted { get; set; }
public DateTime? WizardCompletedAt { get; set; }
public string? WizardCompletedByName { get; set; }
}
/// <summary>
/// DTO for creating a new company
/// </summary>
public class CreateCompanyDto
{
[Required(ErrorMessage = "Company name is required")]
[StringLength(200, ErrorMessage = "Company name cannot exceed 200 characters")]
public string CompanyName { get; set; } = string.Empty;
[StringLength(10, ErrorMessage = "Company code cannot exceed 10 characters")]
public string? CompanyCode { get; set; }
[Required(ErrorMessage = "Primary contact name is required")]
[StringLength(100, ErrorMessage = "Contact name cannot exceed 100 characters")]
public string PrimaryContactName { get; set; } = string.Empty;
[Required(ErrorMessage = "Primary contact email is required")]
[EmailAddress(ErrorMessage = "Invalid email address")]
public string PrimaryContactEmail { get; set; } = string.Empty;
[Phone(ErrorMessage = "Invalid phone number")]
public string? Phone { get; set; }
public string? Address { get; set; }
public string? City { get; set; }
[StringLength(2, ErrorMessage = "State code must be 2 characters")]
public string? State { get; set; }
public string? ZipCode { get; set; }
public bool IsActive { get; set; } = true;
[Required(ErrorMessage = "Subscription start date is required")]
public DateTime SubscriptionStartDate { get; set; } = DateTime.UtcNow;
public DateTime? SubscriptionEndDate { get; set; }
public int SubscriptionPlan { get; set; } = 0;
public string? TimeZone { get; set; } = "America/New_York";
// Initial admin user details
[Required(ErrorMessage = "Admin first name is required")]
public string AdminFirstName { get; set; } = string.Empty;
[Required(ErrorMessage = "Admin last name is required")]
public string AdminLastName { get; set; } = string.Empty;
[Required(ErrorMessage = "Admin email is required")]
[EmailAddress(ErrorMessage = "Invalid email address")]
public string AdminEmail { get; set; } = string.Empty;
[Required(ErrorMessage = "Admin password is required")]
[StringLength(100, MinimumLength = 8, ErrorMessage = "Password must be at least 8 characters long")]
[RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&#^()_+=\-\[\]{};':""\\|,.<>\/`~])[A-Za-z\d@$!%*?&#^()_+=\-\[\]{};':""\\|,.<>\/`~]{8,}$",
ErrorMessage = "Password must contain at least one uppercase letter, one lowercase letter, one digit, and one special character")]
public string AdminPassword { get; set; } = string.Empty;
}
/// <summary>
/// DTO for updating an existing company
/// </summary>
public class UpdateCompanyDto
{
public int Id { get; set; }
public string CompanyName { get; set; } = string.Empty;
public string? CompanyCode { get; set; }
public string PrimaryContactName { get; set; } = string.Empty;
public string PrimaryContactEmail { get; set; } = string.Empty;
public string? Phone { get; set; }
public string? Address { get; set; }
public string? City { get; set; }
public string? State { get; set; }
public string? ZipCode { get; set; }
public bool IsActive { get; set; }
public DateTime SubscriptionStartDate { get; set; }
public DateTime? SubscriptionEndDate { get; set; }
public int SubscriptionPlan { get; set; }
// AI feature flags
public bool AiPhotoQuotesEnabled { get; set; }
public bool AiInventoryAssistEnabled { get; set; }
public int? MaxAiPhotoQuotesPerMonthOverride { get; set; }
// Per-company feature overrides (null = use plan default)
public bool? OnlinePaymentsOverride { get; set; }
public bool? AccountingOverride { get; set; }
public string? TimeZone { get; set; }
}
/// <summary>
/// DTO for creating a company admin user (SuperAdmin only)
/// </summary>
public class CreateCompanyAdminDto
{
public int CompanyId { get; set; }
[Required]
public string CompanyName { get; set; } = string.Empty;
[Required(ErrorMessage = "First name is required")]
[StringLength(100, ErrorMessage = "First name cannot exceed 100 characters")]
[Display(Name = "First Name")]
public string FirstName { get; set; } = string.Empty;
[Required(ErrorMessage = "Last name is required")]
[StringLength(100, ErrorMessage = "Last name cannot exceed 100 characters")]
[Display(Name = "Last Name")]
public string LastName { get; set; } = string.Empty;
[Required(ErrorMessage = "Email is required")]
[EmailAddress(ErrorMessage = "Invalid email address")]
[Display(Name = "Email")]
public string Email { get; set; } = string.Empty;
[Required(ErrorMessage = "Password is required")]
[StringLength(100, MinimumLength = 8, ErrorMessage = "Password must be at least 8 characters long")]
[RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&#^()_+=\-\[\]{};':""\\|,.<>\/`~])[A-Za-z\d@$!%*?&#^()_+=\-\[\]{};':""\\|,.<>\/`~]{8,}$",
ErrorMessage = "Password must contain at least one uppercase letter, one lowercase letter, one digit, and one special character")]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; } = string.Empty;
[Phone(ErrorMessage = "Invalid phone number")]
[Display(Name = "Phone")]
public string? Phone { get; set; }
[StringLength(100, ErrorMessage = "Department cannot exceed 100 characters")]
[Display(Name = "Department")]
public string? Department { get; set; }
[StringLength(100, ErrorMessage = "Position cannot exceed 100 characters")]
[Display(Name = "Position")]
public string? Position { get; set; }
}
/// <summary>
/// Simplified company info DTO for PDF generation (avoids circular dependencies)
/// </summary>
public class CompanyInfoDto
{
public string CompanyName { get; set; } = string.Empty;
public string? Phone { get; set; }
public string? Address { get; set; }
public string? City { get; set; }
public string? State { get; set; }
public string? ZipCode { get; set; }
public string? PrimaryContactEmail { get; set; }
}
@@ -0,0 +1,138 @@
using System.ComponentModel.DataAnnotations;
namespace PowderCoating.Application.DTOs.Company;
public class CompanyPreferencesDto
{
public int Id { get; set; }
public int CompanyId { get; set; }
// Application Defaults
public string DefaultCurrency { get; set; } = "USD";
public string DefaultDateFormat { get; set; } = "MM/dd/yyyy";
public string DefaultTimeFormat { get; set; } = "12h";
public string DefaultPaymentTerms { get; set; } = "Net 30";
public int DefaultQuoteValidityDays { get; set; } = 30;
public string QuoteNumberPrefix { get; set; } = "QT";
public string JobNumberPrefix { get; set; } = "JOB";
public bool UseMetricSystem { get; set; } = false;
// Job / Workflow
public string DefaultJobPriority { get; set; } = "Normal";
public bool RequireCustomerPO { get; set; }
public bool AllowCustomerApproval { get; set; } = true;
public int DefaultTurnaroundDays { get; set; } = 7;
// Email Sender Identity
public string? EmailFromAddress { get; set; }
public string? EmailFromName { get; set; }
// Notifications
public bool EmailNotificationsEnabled { get; set; } = true;
public bool NotifyOnNewJob { get; set; } = true;
public bool NotifyOnNewQuote { get; set; } = true;
public bool NotifyOnJobStatusChange { get; set; } = true;
public bool NotifyOnQuoteApproval { get; set; } = true;
public bool NotifyOnPaymentReceived { get; set; } = true;
public int QuoteExpiryWarningDays { get; set; } = 3;
public int DueDateWarningDays { get; set; } = 2;
public int MaintenanceAlertDays { get; set; } = 7;
public bool PaymentRemindersEnabled { get; set; } = false;
public string PaymentReminderDays { get; set; } = "7,14,30";
// Data Retention
public int QuoteRetentionYears { get; set; } = 7;
public int JobRetentionYears { get; set; } = 7;
public int LogRetentionDays { get; set; } = 90;
public int AutoArchiveJobsDays { get; set; } = 365;
public int DeletedRecordRetentionDays { get; set; } = 30;
// Quote PDF Template
public string QtAccentColor { get; set; } = "#374151";
public string? QtDefaultTerms { get; set; }
public string? QtFooterNote { get; set; }
// Invoice PDF Template
public string InAccentColor { get; set; } = "#374151";
public string? InDefaultTerms { get; set; }
public string? InFooterNote { get; set; }
// Blank Work Order PDF Template
public string WoAccentColor { get; set; } = "#374151";
public string? WoTerms { get; set; }
}
public class UpdateAppDefaultsDto
{
[Required, StringLength(3)] public string DefaultCurrency { get; set; } = "USD";
[Required, StringLength(20)] public string DefaultDateFormat { get; set; } = "MM/dd/yyyy";
[Required] public string DefaultTimeFormat { get; set; } = "12h";
[Required, StringLength(50)] public string DefaultPaymentTerms { get; set; } = "Net 30";
[Range(1, 365)] public int DefaultQuoteValidityDays { get; set; } = 30;
[Required, StringLength(10)] public string QuoteNumberPrefix { get; set; } = "QT";
[Required, StringLength(10)] public string JobNumberPrefix { get; set; } = "JOB";
public bool UseMetricSystem { get; set; } = false;
}
public class UpdateJobDefaultsDto
{
[Required] public string DefaultJobPriority { get; set; } = "Normal";
public bool RequireCustomerPO { get; set; }
public bool AllowCustomerApproval { get; set; }
[Range(1, 365)] public int DefaultTurnaroundDays { get; set; } = 7;
}
public class UpdateNotificationsDto
{
[EmailAddress(ErrorMessage = "Invalid email address format.")]
[StringLength(100)]
[Display(Name = "From Email Address")]
public string? EmailFromAddress { get; set; }
[StringLength(100)]
[Display(Name = "From Display Name")]
public string? EmailFromName { get; set; }
public bool EmailNotificationsEnabled { get; set; }
public bool NotifyOnNewJob { get; set; }
public bool NotifyOnNewQuote { get; set; }
public bool NotifyOnJobStatusChange { get; set; }
public bool NotifyOnQuoteApproval { get; set; }
public bool NotifyOnPaymentReceived { get; set; }
[Range(0, 30)] public int QuoteExpiryWarningDays { get; set; } = 3;
[Range(0, 30)] public int DueDateWarningDays { get; set; } = 2;
[Range(0, 90)] public int MaintenanceAlertDays { get; set; } = 7;
public bool PaymentRemindersEnabled { get; set; } = false;
[StringLength(50)] public string PaymentReminderDays { get; set; } = "7,14,30";
}
public class UpdateDataRetentionDto
{
[Range(1, 99)] public int QuoteRetentionYears { get; set; } = 7;
[Range(1, 99)] public int JobRetentionYears { get; set; } = 7;
[Range(30, 3650)] public int LogRetentionDays { get; set; } = 90;
[Range(30, 3650)] public int AutoArchiveJobsDays { get; set; } = 365;
[Range(7, 365)] public int DeletedRecordRetentionDays { get; set; } = 30;
}
public class UpdateQuoteTemplateDto
{
[RegularExpression(@"^#[0-9A-Fa-f]{6}$", ErrorMessage = "Accent color must be a valid hex color (e.g. #374151)")]
public string QtAccentColor { get; set; } = "#374151";
[StringLength(3000)] public string? QtDefaultTerms { get; set; }
[StringLength(200)] public string? QtFooterNote { get; set; }
}
public class UpdateInvoiceTemplateDto
{
[RegularExpression(@"^#[0-9A-Fa-f]{6}$", ErrorMessage = "Accent color must be a valid hex color (e.g. #374151)")]
public string InAccentColor { get; set; } = "#374151";
[StringLength(3000)] public string? InDefaultTerms { get; set; }
[StringLength(200)] public string? InFooterNote { get; set; }
}
public class UpdateWorkOrderTemplateDto
{
[RegularExpression(@"^#[0-9A-Fa-f]{6}$", ErrorMessage = "Accent color must be a valid hex color (e.g. #374151)")]
public string WoAccentColor { get; set; } = "#374151";
[StringLength(2000)] public string? WoTerms { get; set; }
}
@@ -0,0 +1,323 @@
using System.ComponentModel.DataAnnotations;
using PowderCoating.Application.DTOs.Notification;
using PowderCoating.Application.Services;
using PowderCoating.Core.Enums;
namespace PowderCoating.Application.DTOs.Company
{
/// <summary>
/// Read model for company settings including operating costs
/// </summary>
public class CompanySettingsDto
{
public int Id { get; set; }
public string CompanyName { get; set; } = string.Empty;
public string? CompanyCode { get; set; }
public string PrimaryContactName { get; set; } = string.Empty;
public string PrimaryContactEmail { get; set; } = string.Empty;
public string? Phone { get; set; }
public string? Address { get; set; }
public string? City { get; set; }
public string? State { get; set; }
public string? ZipCode { get; set; }
public string? TimeZone { get; set; }
public bool HasLogo { get; set; }
public CompanyOperatingCostsDto? OperatingCosts { get; set; }
public CompanyPreferencesDto? Preferences { get; set; }
public bool AiPhotoQuotesEnabled { get; set; }
public List<NotificationTemplateDto> NotificationTemplates { get; set; } = new();
// Stripe Connect / online payments
public StripeConnectStatus StripeConnectStatus { get; set; }
public string? StripeAccountId { get; set; }
public OnlinePaymentSurchargeType OnlinePaymentSurchargeType { get; set; }
public decimal OnlinePaymentSurchargeValue { get; set; }
public bool OnlineSurchargeAcknowledged { get; set; }
public bool AllowOnlinePayments { get; set; }
}
/// <summary>
/// DTO for updating basic company information
/// </summary>
public class UpdateCompanySettingsDto
{
[Required(ErrorMessage = "Company name is required")]
[StringLength(200, ErrorMessage = "Company name cannot exceed 200 characters")]
public string CompanyName { get; set; } = string.Empty;
[StringLength(10, ErrorMessage = "Company code cannot exceed 10 characters")]
public string? CompanyCode { get; set; }
[Required(ErrorMessage = "Primary contact name is required")]
[StringLength(100, ErrorMessage = "Contact name cannot exceed 100 characters")]
public string PrimaryContactName { get; set; } = string.Empty;
[Required(ErrorMessage = "Primary contact email is required")]
[EmailAddress(ErrorMessage = "Invalid email format")]
[StringLength(100, ErrorMessage = "Email cannot exceed 100 characters")]
public string PrimaryContactEmail { get; set; } = string.Empty;
[Phone(ErrorMessage = "Invalid phone number format")]
[StringLength(20, ErrorMessage = "Phone number cannot exceed 20 characters")]
public string? Phone { get; set; }
[StringLength(200, ErrorMessage = "Address cannot exceed 200 characters")]
public string? Address { get; set; }
[StringLength(100, ErrorMessage = "City cannot exceed 100 characters")]
public string? City { get; set; }
[StringLength(2, ErrorMessage = "State must be 2 characters")]
public string? State { get; set; }
[StringLength(10, ErrorMessage = "Zip code cannot exceed 10 characters")]
public string? ZipCode { get; set; }
[StringLength(50, ErrorMessage = "Time zone cannot exceed 50 characters")]
public string? TimeZone { get; set; }
}
/// <summary>
/// Read model for company operating costs
/// </summary>
public class CompanyOperatingCostsDto
{
public int Id { get; set; }
public int CompanyId { get; set; }
// Labor Rates
public decimal StandardLaborRate { get; set; }
public decimal AdditionalCoatLaborPercent { get; set; }
// Equipment Operating Costs
public decimal OvenOperatingCostPerHour { get; set; }
public decimal SandblasterCostPerHour { get; set; }
public decimal CoatingBoothCostPerHour { get; set; }
// Material Costs
public decimal PowderCoatingCostPerSqFt { get; set; }
// Tax
public decimal TaxPercent { get; set; }
// Shop Supplies Rate
public decimal ShopSuppliesRate { get; set; }
// Markup / Margin
public PowderCoating.Core.Enums.PricingMode PricingMode { get; set; } = PowderCoating.Core.Enums.PricingMode.MarkupOnMaterial;
public decimal GeneralMarkupPercentage { get; set; }
public decimal TargetMarginPercent { get; set; }
// Rush Charge
public string RushChargeType { get; set; } = "Percentage"; // "Percentage" or "FixedAmount"
public decimal RushChargePercentage { get; set; }
public decimal RushChargeFixedAmount { get; set; }
// Shop Minimum
public decimal ShopMinimumCharge { get; set; }
// Part Complexity Multipliers (%)
public decimal ComplexitySimplePercent { get; set; }
public decimal ComplexityModeratePercent { get; set; }
public decimal ComplexityComplexPercent { get; set; }
public decimal ComplexityExtremePercent { get; set; }
// AI Profile
public string? AiContextProfile { get; set; }
// Shop Capability
public ShopCapabilityTier ShopCapabilityTier { get; set; }
public BlastSetupType BlastSetupType { get; set; }
public decimal CompressorCfm { get; set; }
public int BlastNozzleSize { get; set; }
public BlastSubstrateType PrimaryBlastSubstrate { get; set; }
public decimal? BlastRateSqFtPerHourOverride { get; set; }
public CoatingGunType CoatingGunType { get; set; }
public decimal? CoatingRateSqFtPerHourOverride { get; set; }
/// <summary>Derived blast rate — shown to the user as a sanity-check value.</summary>
public decimal DerivedBlastRateSqFtPerHour { get; set; }
/// <summary>Derived coating rate — shown to the user as a sanity-check value.</summary>
public decimal DerivedCoatingRateSqFtPerHour { get; set; }
}
/// <summary>
/// DTO for updating company operating costs
/// </summary>
public class UpdateOperatingCostsDto
{
// Labor Rates (per hour)
[Required(ErrorMessage = "Standard labor rate is required")]
[Range(0, 10000, ErrorMessage = "Standard labor rate must be between 0 and 10,000")]
[Display(Name = "Standard Labor Rate ($/hr)")]
public decimal StandardLaborRate { get; set; }
[Range(0, 100, ErrorMessage = "Additional coat labor percent must be between 0 and 100")]
[Display(Name = "Additional Coat Labor (%)")]
public decimal AdditionalCoatLaborPercent { get; set; } = 30m;
// Equipment Operating Costs (per hour)
[Required(ErrorMessage = "Oven operating cost is required")]
[Range(0, 10000, ErrorMessage = "Oven operating cost must be between 0 and 10,000")]
[Display(Name = "Oven Operating Cost ($/hr)")]
public decimal OvenOperatingCostPerHour { get; set; }
[Range(0, 10000, ErrorMessage = "Sandblaster cost must be between 0 and 10,000")]
[Display(Name = "Sandblaster Cost ($/hr)")]
public decimal SandblasterCostPerHour { get; set; }
[Range(0, 10000, ErrorMessage = "Coating booth cost must be between 0 and 10,000")]
[Display(Name = "Coating Booth Cost ($/hr)")]
public decimal CoatingBoothCostPerHour { get; set; }
// Material Costs
[Range(0, 1000, ErrorMessage = "Powder coating cost must be between 0 and 1,000")]
[Display(Name = "Powder Coating Cost Per Sq Ft ($/sq ft)")]
public decimal PowderCoatingCostPerSqFt { get; set; }
// Tax
[Range(0, 100, ErrorMessage = "Tax percent must be between 0 and 100")]
[Display(Name = "Tax Percent (%)")]
public decimal TaxPercent { get; set; }
// Shop Supplies Rate
[Range(0, 100, ErrorMessage = "Shop supplies rate must be between 0 and 100")]
[Display(Name = "Shop Supplies Rate (%)")]
public decimal ShopSuppliesRate { get; set; }
// Markup / Margin Mode
[Display(Name = "Pricing Mode")]
public PowderCoating.Core.Enums.PricingMode PricingMode { get; set; } = PowderCoating.Core.Enums.PricingMode.MarkupOnMaterial;
[Range(0, 100, ErrorMessage = "General markup percentage must be between 0 and 100")]
[Display(Name = "General Markup (%)")]
public decimal GeneralMarkupPercentage { get; set; }
[Range(0, 99, ErrorMessage = "Target margin must be between 0 and 99")]
[Display(Name = "Target Margin (%)")]
public decimal TargetMarginPercent { get; set; }
// Rush Charge
[StringLength(20, ErrorMessage = "Rush charge type cannot exceed 20 characters")]
[Display(Name = "Rush Charge Type")]
public string RushChargeType { get; set; } = "Percentage"; // "Percentage" or "FixedAmount"
[Range(0, 100, ErrorMessage = "Rush charge percentage must be between 0 and 100")]
[Display(Name = "Rush Charge (%)")]
public decimal RushChargePercentage { get; set; }
[Range(0, 100000, ErrorMessage = "Rush charge fixed amount must be between 0 and 100,000")]
[Display(Name = "Rush Charge Fixed Amount ($)")]
public decimal RushChargeFixedAmount { get; set; }
// Shop Minimum
[Range(0, 100000, ErrorMessage = "Shop minimum charge must be between 0 and 100,000")]
[Display(Name = "Shop Minimum Charge ($)")]
public decimal ShopMinimumCharge { get; set; }
// Part Complexity Multipliers
[Range(0, 500)]
public decimal ComplexitySimplePercent { get; set; } = 0m;
[Range(0, 500)]
public decimal ComplexityModeratePercent { get; set; } = 5m;
[Range(0, 500)]
public decimal ComplexityComplexPercent { get; set; } = 15m;
[Range(0, 500)]
public decimal ComplexityExtremePercent { get; set; } = 25m;
}
/// <summary>DTO for updating the company AI profile text used for AI Photo Quote calibration.</summary>
public class UpdateAiProfileDto
{
[StringLength(2000, ErrorMessage = "AI profile cannot exceed 2000 characters")]
public string? AiContextProfile { get; set; }
}
/// <summary>DTO for saving the Quoting Calibration / Shop Capability Profile tab.</summary>
public class UpdateBlastProfileDto
{
public ShopCapabilityTier ShopCapabilityTier { get; set; }
public BlastSetupType BlastSetupType { get; set; }
[Range(0, 2000, ErrorMessage = "CFM must be between 0 and 2000")]
public decimal CompressorCfm { get; set; }
[Range(3, 8, ErrorMessage = "Nozzle size must be between #3 and #8")]
public int BlastNozzleSize { get; set; } = 4;
public BlastSubstrateType PrimaryBlastSubstrate { get; set; }
[Range(0, 5000, ErrorMessage = "Blast rate override must be between 0 and 5000")]
public decimal? BlastRateSqFtPerHourOverride { get; set; }
public CoatingGunType CoatingGunType { get; set; }
[Range(0, 5000, ErrorMessage = "Coating rate override must be between 0 and 5000")]
public decimal? CoatingRateSqFtPerHourOverride { get; set; }
}
/// <summary>
/// Response returned after saving or recalculating the blast profile,
/// so the UI can display the freshly derived rates without a page reload.
/// </summary>
public class BlastProfileResultDto
{
public decimal DerivedBlastRate { get; set; }
public decimal DerivedCoatingRate { get; set; }
}
// ── Named blast setups ──────────────────────────────────────────────────────
public class BlastSetupDto
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public BlastSetupType SetupType { get; set; }
public decimal CompressorCfm { get; set; }
public int BlastNozzleSize { get; set; }
public BlastSubstrateType PrimarySubstrate { get; set; }
public decimal? BlastRateSqFtPerHourOverride { get; set; }
public bool IsDefault { get; set; }
public bool IsActive { get; set; }
public int DisplayOrder { get; set; }
public decimal DerivedRate { get; set; }
}
public class SaveBlastSetupDto
{
public int? Id { get; set; }
[Required]
[StringLength(100)]
public string Name { get; set; } = string.Empty;
public BlastSetupType SetupType { get; set; }
[Range(0, 9999)]
public decimal CompressorCfm { get; set; }
[Range(2, 8)]
public int BlastNozzleSize { get; set; } = 5;
public BlastSubstrateType PrimarySubstrate { get; set; }
[Range(0, 99999)]
public decimal? BlastRateSqFtPerHourOverride { get; set; }
public bool IsDefault { get; set; }
public bool IsActive { get; set; } = true;
}
/// <summary>Lightweight summary injected into wizard pages for the blast-setup dropdown.</summary>
public class BlastSetupSummaryDto
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal DerivedRate { get; set; }
public bool IsDefault { get; set; }
}
}
@@ -0,0 +1,40 @@
using System.ComponentModel.DataAnnotations;
namespace PowderCoating.Application.DTOs.Company;
public class OvenCostDto
{
public int Id { get; set; }
public string Label { get; set; } = string.Empty;
public decimal CostPerHour { get; set; }
public bool IsActive { get; set; }
public int DisplayOrder { get; set; }
public decimal? MaxLoadSqFt { get; set; }
public int? DefaultCycleMinutes { get; set; }
}
public class CreateOvenCostDto
{
[Required(ErrorMessage = "Label is required")]
[StringLength(100, ErrorMessage = "Label cannot exceed 100 characters")]
public string Label { get; set; } = string.Empty;
[Required(ErrorMessage = "Cost per hour is required")]
[Range(0, 10000, ErrorMessage = "Cost per hour must be between 0 and 10,000")]
public decimal CostPerHour { get; set; }
public bool IsActive { get; set; } = true;
public int DisplayOrder { get; set; } = 0;
[Range(0, 100000, ErrorMessage = "Capacity must be between 0 and 100,000 sq ft")]
public decimal? MaxLoadSqFt { get; set; }
[Range(1, 1440, ErrorMessage = "Cycle time must be between 1 and 1440 minutes")]
public int? DefaultCycleMinutes { get; set; }
}
public class UpdateOvenCostDto : CreateOvenCostDto
{
public int Id { get; set; }
}
@@ -0,0 +1,11 @@
namespace PowderCoating.Application.DTOs.Company;
/// <summary>
/// Slim DTO passed into PdfService to control quote PDF appearance.
/// </summary>
public class QuoteTemplateSettingsDto
{
public string AccentColor { get; set; } = "#374151";
public string? FooterNote { get; set; }
public string? DefaultTerms { get; set; }
}
@@ -0,0 +1,190 @@
using System.ComponentModel.DataAnnotations;
namespace PowderCoating.Application.DTOs.Customer;
public class CustomerDto
{
public int Id { get; set; }
public string? CompanyName { get; set; }
public string? ContactFirstName { get; set; }
public string? ContactLastName { get; set; }
public string? Email { get; set; }
public string? Phone { get; set; }
public string? MobilePhone { get; set; }
public string? Address { get; set; }
public string? City { get; set; }
public string? State { get; set; }
public string? ZipCode { get; set; }
public string? Country { get; set; }
public bool IsCommercial { get; set; }
public string? TaxId { get; set; }
public decimal CreditLimit { get; set; }
public decimal CurrentBalance { get; set; }
public decimal CreditBalance { get; set; }
public string? PaymentTerms { get; set; }
public int? PricingTierId { get; set; }
public string? PricingTierName { get; set; }
public bool IsTaxExempt { get; set; }
public bool HasTaxExemptCertificate { get; set; }
public string? TaxExemptCertificateFileName { get; set; }
public string? GeneralNotes { get; set; }
public bool IsActive { get; set; }
public DateTime? LastContactDate { get; set; }
public DateTime CreatedAt { get; set; }
public bool NotifyByEmail { get; set; }
public bool NotifyBySms { get; set; }
public DateTime? SmsConsentedAt { get; set; }
public string? SmsConsentMethod { get; set; }
}
public class CreateCustomerDto : IValidatableObject
{
[Display(Name = "Company Name")]
[StringLength(200)]
public string? CompanyName { get; set; }
[Display(Name = "First Name")]
[StringLength(100)]
public string? ContactFirstName { get; set; }
[Display(Name = "Last Name")]
[StringLength(100)]
public string? ContactLastName { get; set; }
[Display(Name = "Email")]
[EmailAddress(ErrorMessage = "Please enter a valid email address")]
[StringLength(200)]
public string? Email { get; set; }
[Display(Name = "Phone")]
[Phone(ErrorMessage = "Please enter a valid phone number")]
[StringLength(20)]
public string? Phone { get; set; }
[Display(Name = "Mobile Phone")]
[Phone(ErrorMessage = "Please enter a valid mobile phone number")]
[StringLength(20)]
public string? MobilePhone { get; set; }
[Display(Name = "Street Address")]
[StringLength(500)]
public string? Address { get; set; }
[Display(Name = "City")]
[StringLength(100)]
public string? City { get; set; }
[Display(Name = "State")]
[StringLength(50)]
public string? State { get; set; }
[Display(Name = "Zip Code")]
[StringLength(20)]
public string? ZipCode { get; set; }
[Display(Name = "Country")]
[StringLength(100)]
public string? Country { get; set; } = "USA";
[Display(Name = "Customer Type")]
public bool IsCommercial { get; set; }
[Display(Name = "Tax ID / EIN")]
[StringLength(50)]
public string? TaxId { get; set; }
[Display(Name = "Credit Limit")]
[Range(0, 9999999.99)]
public decimal CreditLimit { get; set; }
[Display(Name = "Payment Terms")]
[StringLength(50)]
public string? PaymentTerms { get; set; }
[Display(Name = "Pricing Tier")]
public int? PricingTierId { get; set; }
[Display(Name = "Tax Exempt")]
public bool IsTaxExempt { get; set; }
[Display(Name = "General Notes")]
[StringLength(2000)]
public string? GeneralNotes { get; set; }
[Display(Name = "Notify by Email")]
public bool NotifyByEmail { get; set; } = true;
[Display(Name = "Notify by SMS")]
public bool NotifyBySms { get; set; } = false;
/// <summary>
/// Form-only flag: staff checks this to record that the customer gave verbal consent to receive SMS.
/// Not mapped to any entity field — the controller handles setting SmsConsentedAt and SmsConsentMethod.
/// </summary>
[Display(Name = "Customer has verbally consented to receive SMS notifications")]
public bool SmsConsentGranted { get; set; } = false;
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// At least one name field is required (Company Name OR Contact First/Last Name)
if (string.IsNullOrWhiteSpace(CompanyName) &&
string.IsNullOrWhiteSpace(ContactFirstName) &&
string.IsNullOrWhiteSpace(ContactLastName))
{
yield return new ValidationResult(
"Please provide either a Company Name or a Contact Name (First and/or Last Name)",
new[] { nameof(CompanyName), nameof(ContactFirstName), nameof(ContactLastName) });
}
// At least one contact method is required (Email OR Phone)
if (string.IsNullOrWhiteSpace(Email) && string.IsNullOrWhiteSpace(Phone))
{
yield return new ValidationResult(
"Please provide at least one contact method (Email or Phone)",
new[] { nameof(Email), nameof(Phone) });
}
}
}
public class UpdateCustomerDto : CreateCustomerDto
{
public int Id { get; set; }
public bool IsActive { get; set; }
public bool HasTaxExemptCertificate { get; set; }
public string? TaxExemptCertificateFileName { get; set; }
// Read from entity so the Edit view can display consent status
public DateTime? SmsConsentedAt { get; set; }
public string? SmsConsentMethod { get; set; }
}
public class CustomerListDto
{
public int Id { get; set; }
public string? CompanyName { get; set; }
public string ContactName { get; set; } = string.Empty;
public string? Phone { get; set; }
public string? Email { get; set; }
public bool IsCommercial { get; set; }
public decimal CurrentBalance { get; set; }
public bool IsActive { get; set; }
public DateTime? LastContactDate { get; set; }
}
public class AddCreditDto
{
[Required]
[Range(0.01, 99999.99, ErrorMessage = "Amount must be between $0.01 and $99,999.99")]
public decimal Amount { get; set; }
[Required]
[StringLength(200, ErrorMessage = "Reason cannot exceed 200 characters")]
[Display(Name = "Reason")]
public string Reason { get; set; } = string.Empty;
[StringLength(1000)]
[Display(Name = "Notes")]
public string? Notes { get; set; }
[Display(Name = "Expiry Date (optional)")]
public DateTime? ExpiryDate { get; set; }
}
@@ -0,0 +1,53 @@
using System.ComponentModel.DataAnnotations;
namespace PowderCoating.Application.DTOs.Customer;
public class PricingTierDto
{
public int Id { get; set; }
public string TierName { get; set; } = string.Empty;
public string? Description { get; set; }
public decimal DiscountPercent { get; set; }
public bool IsActive { get; set; }
public int CustomerCount { get; set; }
}
public class CreatePricingTierDto
{
[Required(ErrorMessage = "Tier name is required")]
[MaxLength(100)]
[Display(Name = "Tier Name")]
public string TierName { get; set; } = string.Empty;
[MaxLength(500)]
public string? Description { get; set; }
[Required]
[Range(0, 100, ErrorMessage = "Discount must be between 0 and 100")]
[Display(Name = "Discount %")]
public decimal DiscountPercent { get; set; }
[Display(Name = "Active")]
public bool IsActive { get; set; } = true;
}
public class UpdatePricingTierDto
{
public int Id { get; set; }
[Required(ErrorMessage = "Tier name is required")]
[MaxLength(100)]
[Display(Name = "Tier Name")]
public string TierName { get; set; } = string.Empty;
[MaxLength(500)]
public string? Description { get; set; }
[Required]
[Range(0, 100, ErrorMessage = "Discount must be between 0 and 100")]
[Display(Name = "Discount %")]
public decimal DiscountPercent { get; set; }
[Display(Name = "Active")]
public bool IsActive { get; set; }
}
@@ -0,0 +1,261 @@
using PowderCoating.Core.Enums; // Still needed for MaintenanceStatus and MaintenancePriority
namespace PowderCoating.Application.DTOs.Dashboard;
public class SuperAdminDashboardViewModel
{
// Platform-wide counts
public int TotalCompanies { get; set; }
public int ActiveCompanies { get; set; }
public int InactiveCompanies { get; set; }
public int TotalUsers { get; set; }
// Subscription breakdown — DB-driven (keyed by plan int value)
public Dictionary<int, (string DisplayName, int Count)> PlanDistribution { get; set; } = new();
public int ActiveSubscriptions { get; set; }
public int GracePeriodCount { get; set; }
public int ExpiredCount { get; set; }
// Companies needing attention (expired or grace period)
public List<PlatformCompanyAlertDto> CompanyAlerts { get; set; } = new();
// Recently registered companies
public List<PlatformRecentCompanyDto> RecentCompanies { get; set; } = new();
}
public class PlatformCompanyAlertDto
{
public int Id { get; set; }
public string CompanyName { get; set; } = string.Empty;
public int Plan { get; set; }
public string PlanDisplayName { get; set; } = string.Empty;
public SubscriptionStatus Status { get; set; }
public DateTime? SubscriptionEndDate { get; set; }
public int DaysOverdue { get; set; }
public bool IsActive { get; set; }
}
public class PlatformRecentCompanyDto
{
public int Id { get; set; }
public string CompanyName { get; set; } = string.Empty;
public int Plan { get; set; }
public string PlanDisplayName { get; set; } = string.Empty;
public SubscriptionStatus Status { get; set; }
public bool IsActive { get; set; }
public DateTime CreatedAt { get; set; }
}
public class DashboardViewModel
{
// Summary counts
public int ActiveJobsCount { get; set; }
public int TodaysJobsCount { get; set; }
public int OverdueJobsCount { get; set; }
public int TodaysAppointmentsCount { get; set; }
public int LowStockCount { get; set; }
public int PendingMaintenanceCount { get; set; }
public int PendingQuotesCount { get; set; }
public decimal PendingQuoteValue { get; set; }
public decimal MonthlyRevenue { get; set; }
public int ActiveCustomersCount { get; set; }
// Financial KPIs
public decimal OutstandingAr { get; set; }
public decimal CollectedThisMonth { get; set; }
public decimal InvoicedThisMonth { get; set; }
public int OverdueInvoicesCount { get; set; }
public decimal OverdueInvoicesAmount { get; set; }
// Tip of the day
public string? TipOfTheDay { get; set; }
// AR Aging buckets (balance due by days past due)
public decimal AgingCurrent { get; set; } // Not yet due
public decimal AgingDays1To30 { get; set; } // 130 days overdue
public decimal AgingDays31To60 { get; set; } // 3160 days overdue
public decimal AgingDays61To90 { get; set; } // 6190 days overdue
public decimal AgingDaysOver90 { get; set; } // 90+ days overdue
// Bills Due
public List<DashboardBillDto> BillsDue { get; set; } = new();
public int BillsDueCount { get; set; }
public decimal BillsDueAmount { get; set; }
// Powder order tracking
public List<PowderOrderVendorGroupDto> PowderOrdersNeeded { get; set; } = new();
public int PowderOrdersNeededCount { get; set; }
// Powder ordered / awaiting receipt
public List<PowderOrderVendorGroupDto> PowderOrdersPlaced { get; set; } = new();
public int PowderOrdersPlacedCount { get; set; }
// Sections
public List<DashboardJobDto> TodaysJobs { get; set; } = new();
public List<DashboardAppointmentDto> TodaysAppointments { get; set; } = new();
public List<DashboardJobDto> OverdueJobs { get; set; } = new();
public List<DashboardQuoteDto> ExpiringQuotes { get; set; } = new();
public List<DashboardJobDto> ActiveJobs { get; set; } = new();
public List<DashboardLowStockDto> LowStockItems { get; set; } = new();
public List<DashboardMaintenanceDto> UpcomingMaintenance { get; set; } = new();
public List<DashboardEquipmentAlertDto> EquipmentAlerts { get; set; } = new();
public List<DashboardQuoteDto> PendingQuotes { get; set; } = new();
public List<DashboardRecentActivityDto> RecentActivity { get; set; } = new();
public List<DashboardInvoiceDto> OverdueInvoices { get; set; } = new();
public List<DashboardPaymentDto> RecentPayments { get; set; } = new();
}
public class DashboardBillDto
{
public int Id { get; set; }
public string BillNumber { get; set; } = string.Empty;
public string VendorName { get; set; } = string.Empty;
public decimal BalanceDue { get; set; }
public DateTime? DueDate { get; set; }
public bool IsOverdue { get; set; }
public int DaysOverdue { get; set; }
}
public class DashboardInvoiceDto
{
public int Id { get; set; }
public string InvoiceNumber { get; set; } = string.Empty;
public string CustomerName { get; set; } = string.Empty;
public decimal Total { get; set; }
public decimal BalanceDue { get; set; }
public DateTime? DueDate { get; set; }
public int DaysOverdue { get; set; }
}
public class DashboardPaymentDto
{
public int Id { get; set; }
public int InvoiceId { get; set; }
public string InvoiceNumber { get; set; } = string.Empty;
public string CustomerName { get; set; } = string.Empty;
public decimal Amount { get; set; }
public DateTime PaymentDate { get; set; }
public string PaymentMethodDisplay { get; set; } = string.Empty;
}
public class DashboardJobDto
{
public int Id { get; set; }
public string JobNumber { get; set; } = string.Empty;
public string CustomerName { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string StatusCode { get; set; } = string.Empty;
public string StatusDisplayName { get; set; } = string.Empty;
public string StatusColorClass { get; set; } = string.Empty;
public string PriorityCode { get; set; } = string.Empty;
public string PriorityDisplayName { get; set; } = string.Empty;
public string PriorityColorClass { get; set; } = string.Empty;
public DateTime? ScheduledDate { get; set; }
public DateTime? DueDate { get; set; }
public string? AssignedWorkerName { get; set; }
}
public class DashboardLowStockDto
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? ColorName { get; set; }
public string? Manufacturer { get; set; }
public decimal QuantityOnHand { get; set; }
public decimal ReorderPoint { get; set; }
public string UnitOfMeasure { get; set; } = string.Empty;
}
public class DashboardMaintenanceDto
{
public int Id { get; set; }
public string EquipmentName { get; set; } = string.Empty;
public string MaintenanceType { get; set; } = string.Empty;
public MaintenanceStatus Status { get; set; }
public MaintenancePriority Priority { get; set; }
public DateTime ScheduledDate { get; set; }
public string Description { get; set; } = string.Empty;
public string? AssignedWorkerName { get; set; }
}
public class DashboardQuoteDto
{
public int Id { get; set; }
public string QuoteNumber { get; set; } = string.Empty;
public string CustomerName { get; set; } = string.Empty;
public DateTime QuoteDate { get; set; }
public DateTime? ExpirationDate { get; set; }
public decimal Total { get; set; }
public string StatusCode { get; set; } = string.Empty;
public string StatusDisplayName { get; set; } = string.Empty;
}
public class DashboardAppointmentDto
{
public int Id { get; set; }
public string AppointmentNumber { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string CustomerName { get; set; } = string.Empty;
public DateTime ScheduledStartTime { get; set; }
public DateTime ScheduledEndTime { get; set; }
public bool IsAllDay { get; set; }
public string TypeDisplayName { get; set; } = string.Empty;
public string TypeColorClass { get; set; } = string.Empty;
public string StatusDisplayName { get; set; } = string.Empty;
public string StatusColorClass { get; set; } = string.Empty;
public string? AssignedWorkerName { get; set; }
}
public class DashboardEquipmentAlertDto
{
public int Id { get; set; }
public string EquipmentName { get; set; } = string.Empty;
public string EquipmentType { get; set; } = string.Empty;
public string Issue { get; set; } = string.Empty;
public string Severity { get; set; } = string.Empty; // "Critical", "Warning", "Info"
public DateTime? LastMaintenanceDate { get; set; }
public DateTime? NextMaintenanceDue { get; set; }
}
public class PowderOrderVendorGroupDto
{
public int? VendorId { get; set; }
public string VendorName { get; set; } = "No Vendor Assigned";
public string? VendorPhone { get; set; }
public string? VendorEmail { get; set; }
public decimal TotalLbsNeeded { get; set; }
public decimal TotalEstCost { get; set; }
public List<PowderOrderLineDto> Lines { get; set; } = new();
}
public class PowderOrderLineDto
{
public int CoatId { get; set; }
public int JobId { get; set; }
public string JobNumber { get; set; } = string.Empty;
public string CustomerName { get; set; } = string.Empty;
public string CoatName { get; set; } = string.Empty;
public string? ColorName { get; set; }
public string? ColorCode { get; set; }
public string? Finish { get; set; }
public string? SKU { get; set; }
public decimal LbsToOrder { get; set; }
public decimal? CostPerLb { get; set; }
public decimal? EstCost => CostPerLb.HasValue ? LbsToOrder * CostPerLb.Value : null;
public DateTime? OrderedAt { get; set; }
public bool HasInventoryItem { get; set; }
public int? VendorId { get; set; }
}
public class DashboardRecentActivityDto
{
public int Id { get; set; }
public string Type { get; set; } = string.Empty; // "Quote", "Job", "Customer"
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string CustomerName { get; set; } = string.Empty;
public DateTime ActivityDate { get; set; }
public string? StatusDisplayName { get; set; }
public string? StatusColorClass { get; set; }
public decimal? Amount { get; set; }
}
@@ -0,0 +1,124 @@
using System.ComponentModel.DataAnnotations;
namespace PowderCoating.Application.DTOs.Equipment;
public class EquipmentDto
{
public int Id { get; set; }
public string EquipmentName { get; set; } = string.Empty;
public string? EquipmentNumber { get; set; }
public string EquipmentType { get; set; } = string.Empty;
public string? Manufacturer { get; set; }
public string? Model { get; set; }
public string? SerialNumber { get; set; }
public DateTime? PurchaseDate { get; set; }
public decimal PurchasePrice { get; set; }
public DateTime? WarrantyExpiration { get; set; }
public string Status { get; set; } = string.Empty;
public string StatusDisplay { get; set; } = string.Empty;
public string? Location { get; set; }
public int RecommendedMaintenanceIntervalDays { get; set; }
public DateTime? LastMaintenanceDate { get; set; }
public DateTime? NextScheduledMaintenance { get; set; }
public int? DaysUntilMaintenance { get; set; }
public string? Notes { get; set; }
public bool IsActive { get; set; }
// Manual file information
public string? ManualFilePath { get; set; }
public string? ManualFileName { get; set; }
public long? ManualFileSize { get; set; }
public string? ManualContentType { get; set; }
public DateTime? ManualUploadedDate { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
}
public class EquipmentListDto
{
public int Id { get; set; }
public string EquipmentName { get; set; } = string.Empty;
public string? EquipmentNumber { get; set; }
public string EquipmentType { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public string StatusDisplay { get; set; } = string.Empty;
public string? Location { get; set; }
public DateTime? NextScheduledMaintenance { get; set; }
public bool IsActive { get; set; }
}
public class CreateEquipmentDto
{
[Required(ErrorMessage = "Equipment name is required")]
[StringLength(200, ErrorMessage = "Equipment name cannot exceed 200 characters")]
[Display(Name = "Equipment Name")]
public string EquipmentName { get; set; } = string.Empty;
[StringLength(50, ErrorMessage = "Equipment number cannot exceed 50 characters")]
[Display(Name = "Equipment Number")]
public string? EquipmentNumber { get; set; }
[Required(ErrorMessage = "Equipment type is required")]
[StringLength(100, ErrorMessage = "Equipment type cannot exceed 100 characters")]
[Display(Name = "Equipment Type")]
public string EquipmentType { get; set; } = string.Empty;
[StringLength(100, ErrorMessage = "Manufacturer cannot exceed 100 characters")]
[Display(Name = "Manufacturer")]
public string? Manufacturer { get; set; }
[StringLength(100, ErrorMessage = "Model cannot exceed 100 characters")]
[Display(Name = "Model")]
public string? Model { get; set; }
[StringLength(100, ErrorMessage = "Serial number cannot exceed 100 characters")]
[Display(Name = "Serial Number")]
public string? SerialNumber { get; set; }
[Display(Name = "Purchase Date")]
public DateTime? PurchaseDate { get; set; }
[Range(0, 9999999.99, ErrorMessage = "Purchase price must be between 0 and 9,999,999.99")]
[Display(Name = "Purchase Price")]
public decimal PurchasePrice { get; set; }
[Display(Name = "Warranty Expiration")]
public DateTime? WarrantyExpiration { get; set; }
[Required(ErrorMessage = "Status is required")]
[StringLength(50, ErrorMessage = "Status cannot exceed 50 characters")]
[Display(Name = "Status")]
public string Status { get; set; } = "Operational";
[StringLength(200, ErrorMessage = "Location cannot exceed 200 characters")]
[Display(Name = "Location")]
public string? Location { get; set; }
[Range(1, 3650, ErrorMessage = "Maintenance interval must be between 1 and 3650 days")]
[Display(Name = "Recommended Maintenance Interval (Days)")]
public int RecommendedMaintenanceIntervalDays { get; set; }
[Display(Name = "Last Maintenance Date")]
public DateTime? LastMaintenanceDate { get; set; }
[Display(Name = "Next Scheduled Maintenance")]
public DateTime? NextScheduledMaintenance { get; set; }
[StringLength(2000, ErrorMessage = "Notes cannot exceed 2000 characters")]
[Display(Name = "Notes")]
public string? Notes { get; set; }
}
public class UpdateEquipmentDto : CreateEquipmentDto
{
[Required]
public int Id { get; set; }
[Display(Name = "Active")]
public bool IsActive { get; set; }
}
@@ -0,0 +1,89 @@
using System.ComponentModel.DataAnnotations;
using PowderCoating.Core.Enums;
namespace PowderCoating.Application.DTOs.GiftCertificate;
public class GiftCertificateListDto
{
public int Id { get; set; }
public string CertificateCode { get; set; } = string.Empty;
public decimal OriginalAmount { get; set; }
public decimal RedeemedAmount { get; set; }
public decimal RemainingBalance { get; set; }
public string? RecipientName { get; set; }
public string? RecipientEmail { get; set; }
public GiftCertificateIssuedReason IssuedReason { get; set; }
public GiftCertificateStatus Status { get; set; }
public DateTime IssueDate { get; set; }
public DateTime? ExpiryDate { get; set; }
}
public class GiftCertificateDto : GiftCertificateListDto
{
public int? RecipientCustomerId { get; set; }
public decimal? PurchasePrice { get; set; }
public int? PurchasingCustomerId { get; set; }
public string? PurchasingCustomerName { get; set; }
public string? Notes { get; set; }
public string? IssuedByName { get; set; }
public List<GiftCertificateRedemptionDto> Redemptions { get; set; } = new();
}
public class GiftCertificateRedemptionDto
{
public int Id { get; set; }
public int GiftCertificateId { get; set; }
public int InvoiceId { get; set; }
public string InvoiceNumber { get; set; } = string.Empty;
public decimal AmountRedeemed { get; set; }
public DateTime RedeemedDate { get; set; }
public string? RedeemedByName { get; set; }
}
public class CreateGiftCertificateDto
{
[Required]
[Range(1.00, 9999.99, ErrorMessage = "Amount must be between $1.00 and $9,999.99")]
[Display(Name = "Certificate Amount")]
public decimal Amount { get; set; }
[Required]
[Display(Name = "Issued Reason")]
public GiftCertificateIssuedReason IssuedReason { get; set; } = GiftCertificateIssuedReason.Sold;
[Range(0, 9999.99)]
[Display(Name = "Purchase Price (if sold)")]
public decimal? PurchasePrice { get; set; }
[Display(Name = "Purchasing Customer")]
public int? PurchasingCustomerId { get; set; }
[Display(Name = "Recipient Customer")]
public int? RecipientCustomerId { get; set; }
[StringLength(200)]
[Display(Name = "Recipient Name")]
public string? RecipientName { get; set; }
[EmailAddress]
[StringLength(200)]
[Display(Name = "Recipient Email")]
public string? RecipientEmail { get; set; }
[Display(Name = "Expiry Date (optional)")]
public DateTime? ExpiryDate { get; set; }
[StringLength(1000)]
[Display(Name = "Notes")]
public string? Notes { get; set; }
}
public class RedeemGiftCertificateDto
{
[Required]
public string CertificateCode { get; set; } = string.Empty;
[Required]
[Range(0.01, 9999.99)]
public decimal Amount { get; set; }
}
@@ -0,0 +1,34 @@
namespace PowderCoating.Application.DTOs.Health;
public enum ConfigIssueSeverity { Info, Warning, Critical }
/// <summary>
/// A single configuration gap detected for a tenant company.
/// </summary>
public class ConfigHealthIssue
{
public string Code { get; set; } = "";
public string Title { get; set; } = "";
public string Detail { get; set; } = "";
public ConfigIssueSeverity Severity { get; set; }
public string? FixPath { get; set; }
public string? FixLabel { get; set; }
}
/// <summary>
/// Aggregate config health result for one company.
/// </summary>
public class CompanyConfigHealth
{
public int CompanyId { get; set; }
public List<ConfigHealthIssue> Issues { get; set; } = new();
public bool IsHealthy => Issues.Count == 0;
public int CriticalCount => Issues.Count(i => i.Severity == ConfigIssueSeverity.Critical);
public int WarningCount => Issues.Count(i => i.Severity == ConfigIssueSeverity.Warning);
public int InfoCount => Issues.Count(i => i.Severity == ConfigIssueSeverity.Info);
public ConfigIssueSeverity OverallSeverity => Issues.Count == 0
? ConfigIssueSeverity.Info
: Issues.Max(i => i.Severity);
}
@@ -0,0 +1,39 @@
using CsvHelper.Configuration.Attributes;
namespace PowderCoating.Application.DTOs.Import;
/// <summary>
/// DTO for importing appointments from CSV files.
/// </summary>
public class AppointmentImportDto
{
[Name("AppointmentNumber")]
public string? AppointmentNumber { get; set; }
[Name("CustomerEmail")]
public string? CustomerEmail { get; set; }
[Name("AppointmentType")]
public string AppointmentType { get; set; } = string.Empty;
[Name("Status")]
public string Status { get; set; } = "Scheduled";
[Name("ScheduledStart")]
public DateTime ScheduledStart { get; set; }
[Name("ScheduledEnd")]
public DateTime ScheduledEnd { get; set; }
[Name("Title")]
public string Title { get; set; } = string.Empty;
[Name("Description")]
public string? Description { get; set; }
[Name("Location")]
public string? Location { get; set; }
[Name("Notes")]
public string? Notes { get; set; }
}
@@ -0,0 +1,39 @@
using CsvHelper.Configuration.Attributes;
namespace PowderCoating.Application.DTOs.Import;
/// <summary>
/// DTO for importing catalog items from CSV files.
/// </summary>
public class CatalogItemImportDto
{
[Name("CategoryPath")]
public string CategoryPath { get; set; } = string.Empty;
[Name("ItemName")]
public string ItemName { get; set; } = string.Empty;
[Name("SKU")]
public string? SKU { get; set; }
[Name("Description")]
public string? Description { get; set; }
[Name("BasePrice")]
public decimal? BasePrice { get; set; }
[Name("ApproximateArea")]
public decimal? ApproximateArea { get; set; }
[Name("EstimatedMinutes")]
public int? EstimatedMinutes { get; set; }
[Name("RequiresSandblasting")]
public bool? RequiresSandblasting { get; set; }
[Name("RequiresMasking")]
public bool? RequiresMasking { get; set; }
[Name("IsActive")]
public bool? IsActive { get; set; }
}
@@ -0,0 +1,37 @@
using CsvHelper.Configuration.Attributes;
namespace PowderCoating.Application.DTOs.Import;
/// <summary>
/// DTO for importing Chart of Accounts entries from CSV.
/// AccountNumber must be unique per company. System accounts (IsSystem=true) are
/// skipped on import — they are managed by the platform seed data only.
/// </summary>
public class ChartOfAccountsImportDto
{
[Name("AccountNumber")]
public string AccountNumber { get; set; } = string.Empty;
[Name("Name")]
public string Name { get; set; } = string.Empty;
/// <summary>Asset, Liability, Equity, Revenue, CostOfGoods, Expense</summary>
[Name("AccountType")]
public string AccountType { get; set; } = string.Empty;
/// <summary>AccountSubType enum name (e.g. Checking, AccountsReceivable, Sales).</summary>
[Name("AccountSubType")]
public string AccountSubType { get; set; } = string.Empty;
[Name("Description")]
public string? Description { get; set; }
[Name("OpeningBalance")]
public decimal? OpeningBalance { get; set; }
[Name("OpeningBalanceDate")]
public string? OpeningBalanceDate { get; set; }
[Name("IsActive")]
public bool? IsActive { get; set; }
}
@@ -0,0 +1,27 @@
namespace PowderCoating.Application.DTOs.Import;
/// <summary>
/// Result of a CSV bulk import operation.
/// </summary>
public class CsvImportResultDto
{
public bool Success { get; set; }
public int SuccessCount { get; set; }
public int ErrorCount { get; set; }
public int SkippedCount { get; set; }
public int TotalRows { get; set; }
public List<string> Errors { get; set; } = new();
public List<string> Warnings { get; set; } = new();
public string Summary
{
get
{
var parts = new List<string> { $"{SuccessCount} imported" };
if (SkippedCount > 0) parts.Add($"{SkippedCount} skipped (already exist)");
if (ErrorCount > 0) parts.Add($"{ErrorCount} failed");
parts.Add($"{TotalRows} total rows");
return string.Join(", ", parts) + ".";
}
}
}
@@ -0,0 +1,66 @@
using CsvHelper.Configuration.Attributes;
namespace PowderCoating.Application.DTOs.Import;
/// <summary>
/// DTO for importing customers from CSV files.
/// </summary>
public class CustomerImportDto
{
[Name("CompanyName")]
public string CompanyName { get; set; } = string.Empty;
[Name("ContactFirstName")]
public string? ContactFirstName { get; set; }
[Name("ContactLastName")]
public string? ContactLastName { get; set; }
[Name("Email")]
public string? Email { get; set; }
[Name("Phone")]
public string? Phone { get; set; }
[Name("MobilePhone")]
public string? MobilePhone { get; set; }
[Name("Address")]
public string? Address { get; set; }
[Name("City")]
public string? City { get; set; }
[Name("State")]
public string? State { get; set; }
[Name("ZipCode")]
public string? ZipCode { get; set; }
[Name("Country")]
public string? Country { get; set; }
[Name("CustomerType")]
public string? CustomerType { get; set; }
[Name("PricingTierCode")]
public string? PricingTierCode { get; set; }
[Name("CreditLimit")]
public decimal? CreditLimit { get; set; }
[Name("PaymentTerms")]
public string? PaymentTerms { get; set; }
[Name("TaxExempt")]
public bool? TaxExempt { get; set; }
[Name("TaxId")]
public string? TaxId { get; set; }
[Name("IsActive")]
public bool? IsActive { get; set; }
[Name("Notes")]
public string? Notes { get; set; }
}
@@ -0,0 +1,51 @@
using CsvHelper.Configuration.Attributes;
namespace PowderCoating.Application.DTOs.Import;
/// <summary>
/// DTO for importing equipment from CSV files.
/// </summary>
public class EquipmentImportDto
{
[Name("EquipmentName")]
public string EquipmentName { get; set; } = string.Empty;
[Name("EquipmentNumber")]
public string? EquipmentNumber { get; set; }
[Name("EquipmentType")]
public string EquipmentType { get; set; } = string.Empty;
[Name("Manufacturer")]
public string? Manufacturer { get; set; }
[Name("Model")]
public string? Model { get; set; }
[Name("SerialNumber")]
public string? SerialNumber { get; set; }
[Name("PurchaseDate")]
public DateTime? PurchaseDate { get; set; }
[Name("PurchasePrice")]
public decimal? PurchasePrice { get; set; }
[Name("WarrantyExpiration")]
public DateTime? WarrantyExpiration { get; set; }
[Name("Location")]
public string? Location { get; set; }
[Name("RecommendedMaintenanceIntervalDays")]
public int? RecommendedMaintenanceIntervalDays { get; set; }
[Name("Status")]
public string Status { get; set; } = "Operational";
[Name("IsActive")]
public bool? IsActive { get; set; }
[Name("Notes")]
public string? Notes { get; set; }
}
@@ -0,0 +1,46 @@
using CsvHelper.Configuration.Attributes;
namespace PowderCoating.Application.DTOs.Import;
/// <summary>
/// DTO for importing expenses from CSV files.
/// ExpenseNumber is optional — a new EXP-YYMM-#### number is generated when left blank.
/// ExpenseAccountNumber and PaymentAccountNumber are matched against Account.AccountNumber.
/// VendorName is matched against Vendor.CompanyName (optional).
/// JobNumber is matched against Job.JobNumber (optional).
/// PaymentMethod accepts: Cash, Check, CreditDebitCard, BankTransferACH, DigitalPayment.
/// </summary>
public class ExpenseImportDto
{
// Optional — auto-generated if blank
[Name("ExpenseNumber")]
public string? ExpenseNumber { get; set; }
[Name("Date")]
public DateTime Date { get; set; }
// Optional — matched to Vendor by CompanyName
[Name("VendorName")]
public string? VendorName { get; set; }
// Required — matched to Account by AccountNumber (e.g. "6200")
[Name("ExpenseAccountNumber")]
public string ExpenseAccountNumber { get; set; } = string.Empty;
// Required — matched to Account by AccountNumber (e.g. "1000")
[Name("PaymentAccountNumber")]
public string PaymentAccountNumber { get; set; } = string.Empty;
// Optional — matched to Job by JobNumber
[Name("JobNumber")]
public string? JobNumber { get; set; }
[Name("PaymentMethod")]
public string PaymentMethod { get; set; } = "Cash";
[Name("Amount")]
public decimal Amount { get; set; }
[Name("Memo")]
public string? Memo { get; set; }
}
@@ -0,0 +1,91 @@
using CsvHelper.Configuration.Attributes;
namespace PowderCoating.Application.DTOs.Import;
/// <summary>
/// DTO for importing inventory items from CSV files.
/// </summary>
public class InventoryItemImportDto
{
[Name("SKU")]
public string SKU { get; set; } = string.Empty;
// "Name" = old export header; "ItemName" = new export header
[Name("ItemName", "Name")]
public string ItemName { get; set; } = string.Empty;
[Name("Description")]
public string? Description { get; set; }
// "Category" = old export header; "CategoryName" = new export header
[Name("CategoryName", "Category")]
public string? CategoryName { get; set; }
[Name("Manufacturer")]
public string? Manufacturer { get; set; }
[Name("ManufacturerPartNumber")]
public string? ManufacturerPartNumber { get; set; }
// "Color" = old export header; "ColorName" = new export header
[Name("ColorName", "Color")]
public string? ColorName { get; set; }
[Name("ColorCode")]
public string? ColorCode { get; set; }
[Name("Finish")]
public string? Finish { get; set; }
// Vendor name is used to look up PrimaryVendorId
[Name("VendorName")]
public string? VendorName { get; set; }
[Name("VendorPartNumber")]
public string? VendorPartNumber { get; set; }
// "QuantityOnHand" / "Qty on Hand" = old export headers; "QuantityInStock" = new export header
[Name("QuantityInStock", "QuantityOnHand", "Qty on Hand")]
public decimal? QuantityInStock { get; set; }
// "Unit" = old export header; "UnitOfMeasure" = new export header
[Name("UnitOfMeasure", "Unit")]
public string? UnitOfMeasure { get; set; }
// "Unit Cost" = old export header; "UnitCost" = new export header
[Name("UnitCost", "Unit Cost")]
public decimal? UnitCost { get; set; }
[Name("LastPurchasePrice")]
public decimal? LastPurchasePrice { get; set; }
// "Reorder Point" = old export header; "ReorderPoint" = new export header
[Name("ReorderPoint", "Reorder Point")]
public decimal? ReorderPoint { get; set; }
[Name("ReorderQuantity")]
public decimal? ReorderQuantity { get; set; }
[Name("MinimumStock")]
public decimal? MinimumStock { get; set; }
[Name("MaximumStock")]
public decimal? MaximumStock { get; set; }
// Powder-specific coverage fields
[Name("CoverageSqFtPerLb")]
public decimal? CoverageSqFtPerLb { get; set; }
[Name("TransferEfficiencyPct")]
public decimal? TransferEfficiencyPct { get; set; }
[Name("Location")]
public string? Location { get; set; }
// "Active" = old export header (Yes/No); "IsActive" = new export header (true/false)
[Name("IsActive", "Active")]
public bool? IsActive { get; set; }
[Name("Notes")]
public string? Notes { get; set; }
}
@@ -0,0 +1,66 @@
using CsvHelper.Configuration.Attributes;
namespace PowderCoating.Application.DTOs.Import;
/// <summary>
/// DTO for importing invoice headers from CSV files.
/// Column names match the native CSV export (ExportInvoicesCsv) for round-trip compatibility.
/// CustomerEmail is an additional optional column for more reliable customer matching when
/// the file did not originate from the native export.
/// </summary>
public class InvoiceImportDto
{
[Name("InvoiceNumber")]
public string? InvoiceNumber { get; set; }
// Customer resolution: email takes priority, name is the fallback
[Name("CustomerEmail")]
public string? CustomerEmail { get; set; }
// "Customer" matches the column written by ExportInvoicesCsv
[Name("Customer")]
public string? CustomerName { get; set; }
[Name("JobNumber")]
public string? JobNumber { get; set; }
[Name("Status")]
public string Status { get; set; } = "Draft";
[Name("InvoiceDate")]
public DateTime InvoiceDate { get; set; }
[Name("DueDate")]
public DateTime? DueDate { get; set; }
[Name("SubTotal")]
public decimal SubTotal { get; set; }
[Name("TaxPercent")]
public decimal TaxPercent { get; set; }
[Name("TaxAmount")]
public decimal TaxAmount { get; set; }
[Name("DiscountAmount")]
public decimal DiscountAmount { get; set; }
[Name("Total")]
public decimal Total { get; set; }
[Name("AmountPaid")]
public decimal AmountPaid { get; set; }
// BalanceDue is computed (Total - AmountPaid); ignored on import
[Ignore]
public decimal BalanceDue { get; set; }
[Name("CustomerPO")]
public string? CustomerPO { get; set; }
[Name("Terms")]
public string? Terms { get; set; }
[Name("Notes")]
public string? Notes { get; set; }
}
@@ -0,0 +1,49 @@
using CsvHelper.Configuration.Attributes;
namespace PowderCoating.Application.DTOs.Import;
/// <summary>
/// DTO for importing jobs from CSV files.
/// CustomerEmail is the preferred customer lookup key; CustomerName is used as a fallback
/// when the customer has no email address on file (the importer tries email first, then name).
/// </summary>
public class JobImportDto
{
// "Job Number" = old export header (spaced); "JobNumber" = current header
[Name("JobNumber", "Job Number")]
public string? JobNumber { get; set; }
// Optional — importer falls back to CustomerName when blank
[Name("CustomerEmail")]
public string? CustomerEmail { get; set; }
// Fallback identifier when CustomerEmail is absent; matched against CompanyName
[Name("CustomerName")]
public string? CustomerName { get; set; }
[Name("Status")]
public string Status { get; set; } = "Pending";
[Name("Priority")]
public string Priority { get; set; } = "Normal";
[Name("ScheduledDate")]
public DateTime? ScheduledDate { get; set; }
// "Due Date" = old export header (spaced); "DueDate" = current header
[Name("DueDate", "Due Date")]
public DateTime? DueDate { get; set; }
// "Final Price" = old export header (spaced); "FinalPrice" = current header
[Name("FinalPrice", "Final Price")]
public decimal? FinalPrice { get; set; }
[Name("CustomerPO")]
public string? CustomerPO { get; set; }
[Name("SpecialInstructions")]
public string? SpecialInstructions { get; set; }
[Name("Notes")]
public string? Notes { get; set; }
}
@@ -0,0 +1,45 @@
using CsvHelper.Configuration.Attributes;
namespace PowderCoating.Application.DTOs.Import;
/// <summary>
/// DTO for importing maintenance records from CSV files.
/// </summary>
public class MaintenanceImportDto
{
[Name("EquipmentName")]
public string EquipmentName { get; set; } = string.Empty;
[Name("MaintenanceType")]
public string MaintenanceType { get; set; } = string.Empty;
[Name("ScheduledDate")]
public DateTime ScheduledDate { get; set; }
[Name("CompletedDate")]
public DateTime? CompletedDate { get; set; }
[Name("Status")]
public string Status { get; set; } = "Scheduled";
[Name("Priority")]
public string Priority { get; set; } = "Normal";
[Name("LaborCost")]
public decimal? LaborCost { get; set; }
[Name("PartsCost")]
public decimal? PartsCost { get; set; }
[Name("TotalCost")]
public decimal? TotalCost { get; set; }
[Name("Description")]
public string Description { get; set; } = string.Empty;
[Name("WorkPerformed")]
public string? WorkPerformed { get; set; }
[Name("Notes")]
public string? Notes { get; set; }
}
@@ -0,0 +1,32 @@
using CsvHelper.Configuration.Attributes;
namespace PowderCoating.Application.DTOs.Import;
/// <summary>
/// DTO for importing payment records from CSV. Column names match the native
/// ExportPaymentsCsv output for round-trip compatibility. Payments are matched
/// to invoices by InvoiceNumber; duplicates are detected by InvoiceNumber + PaymentDate + Amount.
/// </summary>
public class PaymentImportDto
{
[Name("InvoiceNumber")]
public string? InvoiceNumber { get; set; }
[Name("Amount")]
public decimal Amount { get; set; }
[Name("PaymentDate")]
public DateTime PaymentDate { get; set; }
/// <summary>
/// Valid values: Cash, Check, CreditDebitCard, BankTransferACH, DigitalPayment
/// </summary>
[Name("PaymentMethod")]
public string PaymentMethod { get; set; } = "Cash";
[Name("Reference")]
public string? Reference { get; set; }
[Name("Notes")]
public string? Notes { get; set; }
}
@@ -0,0 +1,21 @@
using CsvHelper.Configuration.Attributes;
namespace PowderCoating.Application.DTOs.Import;
/// <summary>
/// DTO for importing prep services from CSV files.
/// </summary>
public class PrepServiceImportDto
{
[Name("ServiceName")]
public string ServiceName { get; set; } = string.Empty;
[Name("Description")]
public string? Description { get; set; }
[Name("DisplayOrder")]
public int? DisplayOrder { get; set; }
[Name("IsActive")]
public bool? IsActive { get; set; }
}
@@ -0,0 +1,44 @@
using CsvHelper.Configuration.Attributes;
namespace PowderCoating.Application.DTOs.Import;
/// <summary>
/// DTO for importing purchase order headers from CSV. Column names match the native
/// ExportPurchaseOrdersCsv output for round-trip compatibility. Upsert key is PoNumber.
/// </summary>
public class PurchaseOrderImportDto
{
[Name("PoNumber")]
public string? PoNumber { get; set; }
/// <summary>Vendor company name — must match an existing vendor record.</summary>
[Name("Vendor")]
public string? Vendor { get; set; }
/// <summary>
/// Valid values: Draft, Submitted, PartiallyReceived, Received, Cancelled
/// </summary>
[Name("Status")]
public string Status { get; set; } = "Draft";
[Name("OrderDate")]
public DateTime OrderDate { get; set; }
[Name("ExpectedDeliveryDate")]
public DateTime? ExpectedDeliveryDate { get; set; }
[Name("ReceivedDate")]
public DateTime? ReceivedDate { get; set; }
[Name("SubTotal")]
public decimal SubTotal { get; set; }
[Name("ShippingCost")]
public decimal ShippingCost { get; set; }
[Name("TotalAmount")]
public decimal TotalAmount { get; set; }
[Name("Notes")]
public string? Notes { get; set; }
}
@@ -0,0 +1,55 @@
using CsvHelper.Configuration.Attributes;
namespace PowderCoating.Application.DTOs.Import;
/// <summary>
/// DTO for importing quotes from CSV files.
/// </summary>
public class QuoteImportDto
{
[Name("QuoteNumber")]
public string? QuoteNumber { get; set; }
[Name("CustomerEmail")]
public string? CustomerEmail { get; set; }
// Fallback when CustomerEmail is absent; matched against CompanyName then ContactName
[Name("CustomerName")]
public string? CustomerName { get; set; }
[Name("ProspectCompany")]
public string? ProspectCompany { get; set; }
[Name("ProspectContact")]
public string? ProspectContact { get; set; }
[Name("ProspectEmail")]
public string? ProspectEmail { get; set; }
[Name("ProspectPhone")]
public string? ProspectPhone { get; set; }
[Name("Status")]
public string Status { get; set; } = "Draft";
[Name("QuoteDate")]
public DateTime QuoteDate { get; set; }
[Name("ExpirationDate")]
public DateTime? ExpirationDate { get; set; }
[Name("Subtotal")]
public decimal Subtotal { get; set; }
[Name("TaxAmount")]
public decimal TaxAmount { get; set; }
[Name("Total")]
public decimal Total { get; set; }
[Name("Notes")]
public string? Notes { get; set; }
[Name("TermsAndConditions")]
public string? TermsAndConditions { get; set; }
}
@@ -0,0 +1,28 @@
using CsvHelper.Configuration.Attributes;
namespace PowderCoating.Application.DTOs.Import;
/// <summary>
/// DTO for importing shop workers from CSV files.
/// Valid Role values: GeneralLabor, Sandblaster, Coater, Masker, QualityControl, OvenOperator, Supervisor, Maintenance
/// </summary>
public class ShopWorkerImportDto
{
[Name("Name")]
public string Name { get; set; } = string.Empty;
[Name("Role")]
public string Role { get; set; } = "GeneralLabor";
[Name("Phone")]
public string? Phone { get; set; }
[Name("Email")]
public string? Email { get; set; }
[Name("IsActive")]
public bool? IsActive { get; set; }
[Name("Notes")]
public string? Notes { get; set; }
}
@@ -0,0 +1,60 @@
using CsvHelper.Configuration.Attributes;
namespace PowderCoating.Application.DTOs.Import;
/// <summary>
/// DTO for importing vendors from CSV files.
/// </summary>
public class VendorImportDto
{
[Name("CompanyName")]
public string CompanyName { get; set; } = string.Empty;
[Name("ContactName")]
public string? ContactName { get; set; }
[Name("Email")]
public string? Email { get; set; }
[Name("Phone")]
public string? Phone { get; set; }
[Name("Address")]
public string? Address { get; set; }
[Name("City")]
public string? City { get; set; }
[Name("State")]
public string? State { get; set; }
[Name("ZipCode")]
public string? ZipCode { get; set; }
[Name("Country")]
public string? Country { get; set; }
[Name("Website")]
public string? Website { get; set; }
[Name("AccountNumber")]
public string? AccountNumber { get; set; }
[Name("TaxId")]
public string? TaxId { get; set; }
[Name("PaymentTerms")]
public string? PaymentTerms { get; set; }
[Name("CreditLimit")]
public decimal? CreditLimit { get; set; }
[Name("IsPreferred")]
public bool? IsPreferred { get; set; }
[Name("IsActive")]
public bool? IsActive { get; set; }
[Name("Notes")]
public string? Notes { get; set; }
}
@@ -0,0 +1,262 @@
using System.ComponentModel.DataAnnotations;
namespace PowderCoating.Application.DTOs.Inventory;
public class InventoryItemDto
{
public int Id { get; set; }
public string SKU { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public int? InventoryCategoryId { get; set; }
public string? CategoryName { get; set; }
public string Category { get; set; } = string.Empty; // Legacy field
public string? ColorName { get; set; }
public string? ColorCode { get; set; }
public string? Finish { get; set; }
public string? Manufacturer { get; set; }
public string? ManufacturerPartNumber { get; set; }
public decimal? CoverageSqFtPerLb { get; set; }
public decimal? TransferEfficiency { get; set; }
public int? CureTemperatureF { get; set; }
public int? CureTimeMinutes { get; set; }
public string? ColorFamilies { get; set; }
public bool RequiresClearCoat { get; set; }
public string? SpecPageUrl { get; set; }
public decimal QuantityOnHand { get; set; }
public string UnitOfMeasure { get; set; } = "lbs";
public decimal ReorderPoint { get; set; }
public decimal ReorderQuantity { get; set; }
public decimal MinimumStock { get; set; }
public decimal MaximumStock { get; set; }
public decimal UnitCost { get; set; }
public decimal AverageCost { get; set; }
public decimal LastPurchasePrice { get; set; }
public DateTime? LastPurchaseDate { get; set; }
public int? PrimaryVendorId { get; set; }
public string? PrimaryVendorName { get; set; }
public string? VendorPartNumber { get; set; }
public string? Location { get; set; }
public string? Notes { get; set; }
public bool IsActive { get; set; }
public DateTime? DiscontinuedDate { get; set; }
public DateTime CreatedAt { get; set; }
public bool IsLowStock { get; set; }
public bool IsOutOfStock { get; set; }
public bool HasSamplePanel { get; set; }
[Display(Name = "Inventory Asset Account")]
public int? InventoryAccountId { get; set; }
public string? InventoryAccountName { get; set; }
[Display(Name = "COGS Account")]
public int? CogsAccountId { get; set; }
public string? CogsAccountName { get; set; }
}
public class InventoryListDto
{
public int Id { get; set; }
public string SKU { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public int? InventoryCategoryId { get; set; }
public string? CategoryName { get; set; }
public string Category { get; set; } = string.Empty; // Legacy field
public string? ColorName { get; set; }
public decimal QuantityOnHand { get; set; }
public string UnitOfMeasure { get; set; } = "lbs";
public decimal ReorderPoint { get; set; }
public decimal UnitCost { get; set; }
public int? PrimaryVendorId { get; set; }
public string? PrimaryVendorName { get; set; }
public bool IsActive { get; set; }
public bool IsLowStock { get; set; }
public bool IsOutOfStock { get; set; }
public bool HasSamplePanel { get; set; }
}
public class CreateInventoryItemDto
{
[Required(ErrorMessage = "SKU is required")]
[StringLength(50, ErrorMessage = "SKU cannot exceed 50 characters")]
[Display(Name = "SKU")]
public string SKU { get; set; } = string.Empty;
[Required(ErrorMessage = "Name is required")]
[StringLength(200, ErrorMessage = "Name cannot exceed 200 characters")]
[Display(Name = "Item Name")]
public string Name { get; set; } = string.Empty;
[StringLength(2000, ErrorMessage = "Description cannot exceed 2000 characters")]
[Display(Name = "Description")]
public string? Description { get; set; }
[Display(Name = "Category")]
public int? InventoryCategoryId { get; set; }
[StringLength(100, ErrorMessage = "Category cannot exceed 100 characters")]
[Display(Name = "Legacy Category")]
public string Category { get; set; } = string.Empty; // Legacy field - kept for backward compatibility
[StringLength(100, ErrorMessage = "Color name cannot exceed 100 characters")]
[Display(Name = "Color Name")]
public string? ColorName { get; set; }
[StringLength(50, ErrorMessage = "Color code cannot exceed 50 characters")]
[Display(Name = "Color Code")]
public string? ColorCode { get; set; }
[StringLength(50, ErrorMessage = "Finish cannot exceed 50 characters")]
[Display(Name = "Finish")]
public string? Finish { get; set; }
[StringLength(100, ErrorMessage = "Manufacturer cannot exceed 100 characters")]
[Display(Name = "Manufacturer")]
public string? Manufacturer { get; set; }
[StringLength(100, ErrorMessage = "Manufacturer part number cannot exceed 100 characters")]
[Display(Name = "Manufacturer Part Number")]
public string? ManufacturerPartNumber { get; set; }
[Range(0, 10000, ErrorMessage = "Coverage must be between 0 and 10,000 sq ft/lb")]
[Display(Name = "Coverage (Sq Ft/Lb)")]
public decimal? CoverageSqFtPerLb { get; set; }
[Range(0, 100, ErrorMessage = "Transfer efficiency must be between 0 and 100%")]
[Display(Name = "Transfer Efficiency (%)")]
public decimal? TransferEfficiency { get; set; }
[Range(200, 500, ErrorMessage = "Cure temperature must be between 200°F and 500°F")]
[Display(Name = "Cure Temperature (°F)")]
public int? CureTemperatureF { get; set; }
[Range(1, 120, ErrorMessage = "Cure time must be between 1 and 120 minutes")]
[Display(Name = "Cure Time (minutes)")]
public int? CureTimeMinutes { get; set; }
[Display(Name = "Color Families")]
public string? ColorFamilies { get; set; }
[Display(Name = "Requires Clear Coat")]
public bool RequiresClearCoat { get; set; }
[StringLength(500, ErrorMessage = "URL cannot exceed 500 characters")]
[Display(Name = "Product URL")]
public string? SpecPageUrl { get; set; }
[Range(0, 999999999, ErrorMessage = "Quantity on hand must be 0 or greater")]
[Display(Name = "Quantity on Hand")]
public decimal QuantityOnHand { get; set; }
[Required(ErrorMessage = "Unit of measure is required")]
[StringLength(20, ErrorMessage = "Unit of measure cannot exceed 20 characters")]
[Display(Name = "Unit of Measure")]
public string UnitOfMeasure { get; set; } = "lbs";
[Range(0, 999999999, ErrorMessage = "Reorder point must be 0 or greater")]
[Display(Name = "Reorder Point")]
public decimal ReorderPoint { get; set; }
[Range(0, 999999999, ErrorMessage = "Reorder quantity must be 0 or greater")]
[Display(Name = "Reorder Quantity")]
public decimal ReorderQuantity { get; set; }
[Range(0, 999999999, ErrorMessage = "Minimum stock must be 0 or greater")]
[Display(Name = "Minimum Stock")]
public decimal MinimumStock { get; set; }
[Range(0, 999999999, ErrorMessage = "Maximum stock must be 0 or greater")]
[Display(Name = "Maximum Stock")]
public decimal MaximumStock { get; set; }
[Range(0, 9999999.99, ErrorMessage = "Unit cost must be between 0 and 9,999,999.99")]
[Display(Name = "Unit Cost")]
public decimal UnitCost { get; set; }
[Display(Name = "Primary Vendor")]
public int? PrimaryVendorId { get; set; }
[StringLength(100, ErrorMessage = "Vendor part number cannot exceed 100 characters")]
[Display(Name = "Vendor Part Number")]
public string? VendorPartNumber { get; set; }
[StringLength(200, ErrorMessage = "Location cannot exceed 200 characters")]
[Display(Name = "Location")]
public string? Location { get; set; }
[StringLength(2000, ErrorMessage = "Notes cannot exceed 2000 characters")]
[Display(Name = "Notes")]
public string? Notes { get; set; }
[Display(Name = "Inventory Asset Account")]
public int? InventoryAccountId { get; set; }
[Display(Name = "COGS Account")]
public int? CogsAccountId { get; set; }
[Display(Name = "Sample Panel on Wall")]
public bool HasSamplePanel { get; set; }
}
public class UpdateInventoryItemDto : CreateInventoryItemDto
{
[Required]
public int Id { get; set; }
[Display(Name = "Active")]
public bool IsActive { get; set; }
}
public class InventoryTransactionDto
{
public int Id { get; set; }
public int InventoryItemId { get; set; }
public string ItemName { get; set; } = string.Empty;
public string SKU { get; set; } = string.Empty;
public string TransactionType { get; set; } = string.Empty;
public decimal Quantity { get; set; }
public decimal UnitCost { get; set; }
public decimal TotalCost { get; set; }
public DateTime TransactionDate { get; set; }
public string? Reference { get; set; }
public string? Notes { get; set; }
public decimal BalanceAfter { get; set; }
public int? PurchaseOrderId { get; set; }
public string? PurchaseOrderNumber { get; set; }
public int? JobId { get; set; }
public string? JobNumber { get; set; }
}
public class PowderUsageLogDto
{
public int Id { get; set; }
public int JobId { get; set; }
public string JobNumber { get; set; } = string.Empty;
public string? CustomerName { get; set; }
public int? InventoryItemId { get; set; }
public string? ItemName { get; set; }
public string? SKU { get; set; }
public string? CoatColor { get; set; }
public decimal ActualLbsUsed { get; set; }
public decimal EstimatedLbs { get; set; }
public decimal VarianceLbs { get; set; }
public DateTime RecordedAt { get; set; }
public string? Notes { get; set; }
}
public class InventoryLedgerViewModel
{
public int? InventoryItemId { get; set; }
public string? SelectedItemName { get; set; }
public string? SelectedItemSku { get; set; }
public DateTime? DateFrom { get; set; }
public DateTime? DateTo { get; set; }
public string? TypeFilter { get; set; }
public List<InventoryTransactionDto> Transactions { get; set; } = new();
public List<PowderUsageLogDto> PowderUsageLogs { get; set; } = new();
public List<InventoryListDto> AllItems { get; set; } = new();
public decimal TotalPurchased { get; set; }
public decimal TotalUsed { get; set; }
public decimal TotalAdjusted { get; set; }
}
@@ -0,0 +1,137 @@
using System.ComponentModel.DataAnnotations;
using PowderCoating.Core.Enums;
using PowderCoating.Application.DTOs.GiftCertificate;
namespace PowderCoating.Application.DTOs.Invoice;
public class InvoiceListDto
{
public int Id { get; set; }
public string InvoiceNumber { get; set; } = string.Empty;
public int? JobId { get; set; }
public string? JobNumber { get; set; }
public int CustomerId { get; set; }
public string CustomerName { get; set; } = string.Empty;
public InvoiceStatus Status { get; set; }
public DateTime InvoiceDate { get; set; }
public DateTime? DueDate { get; set; }
public decimal Total { get; set; }
public decimal AmountPaid { get; set; }
public decimal BalanceDue { get; set; }
public bool IsOverdue => Status != InvoiceStatus.Paid && Status != InvoiceStatus.Voided
&& Status != InvoiceStatus.WrittenOff && DueDate.HasValue && DueDate.Value < DateTime.UtcNow;
}
public class InvoiceDto
{
public int Id { get; set; }
public string InvoiceNumber { get; set; } = string.Empty;
public int? JobId { get; set; }
public string? JobNumber { get; set; }
public int CustomerId { get; set; }
public string CustomerName { get; set; } = string.Empty;
public string? CustomerEmail { get; set; }
public string? CustomerPhone { get; set; }
public bool CustomerNotifyByEmail { get; set; }
public string? PreparedById { get; set; }
public string? PreparedByName { get; set; }
public InvoiceStatus Status { get; set; }
public DateTime InvoiceDate { get; set; }
public DateTime? DueDate { get; set; }
public DateTime? SentDate { get; set; }
public DateTime? PaidDate { get; set; }
public decimal SubTotal { get; set; }
public decimal TaxPercent { get; set; }
public decimal TaxAmount { get; set; }
public decimal DiscountAmount { get; set; }
public decimal Total { get; set; }
public decimal AmountPaid { get; set; }
public decimal BalanceDue { get; set; }
public string? Notes { get; set; }
public string? InternalNotes { get; set; }
public string? Terms { get; set; }
public string? CustomerPO { get; set; }
public string? ExternalReference { get; set; }
public int? SalesTaxAccountId { get; set; }
public string? SalesTaxAccountName { get; set; }
public decimal CreditApplied { get; set; }
public decimal GiftCertificateRedeemed { get; set; }
// Online payments (Stripe Connect)
public OnlinePaymentStatus OnlinePaymentStatus { get; set; }
public string? PaymentLinkToken { get; set; }
public DateTime? PaymentLinkExpiresAt { get; set; }
public decimal OnlineAmountPaid { get; set; }
public List<InvoiceItemDto> InvoiceItems { get; set; } = new();
public List<PaymentDtos.PaymentDto> Payments { get; set; } = new();
public List<RefundDto> Refunds { get; set; } = new();
public List<CreditMemoApplicationDto> CreditApplications { get; set; } = new();
public List<GiftCertificateRedemptionDto> GiftCertificateRedemptions { get; set; } = new();
}
public class CreateInvoiceDto
{
public int? JobId { get; set; }
[Range(1, int.MaxValue, ErrorMessage = "Please select a customer.")]
public int CustomerId { get; set; }
public string? PreparedById { get; set; }
public DateTime InvoiceDate { get; set; } = DateTime.Today;
public DateTime? DueDate { get; set; }
public decimal TaxPercent { get; set; }
public decimal DiscountAmount { get; set; }
public string? Notes { get; set; }
public string? InternalNotes { get; set; }
public string? Terms { get; set; }
public string? CustomerPO { get; set; }
public List<CreateInvoiceItemDto> InvoiceItems { get; set; } = new();
}
public class UpdateInvoiceDto
{
public DateTime InvoiceDate { get; set; }
public DateTime? DueDate { get; set; }
public decimal TaxPercent { get; set; }
public decimal DiscountAmount { get; set; }
public string? Notes { get; set; }
public string? InternalNotes { get; set; }
public string? Terms { get; set; }
public string? CustomerPO { get; set; }
public List<CreateInvoiceItemDto> InvoiceItems { get; set; } = new();
}
public class InvoiceItemDto
{
public int Id { get; set; }
public int InvoiceId { get; set; }
public int? SourceJobItemId { get; set; }
public int? CatalogItemId { get; set; }
public string Description { get; set; } = string.Empty;
public decimal Quantity { get; set; }
public decimal UnitPrice { get; set; }
public decimal TotalPrice { get; set; }
public string? ColorName { get; set; }
public string? Notes { get; set; }
public int DisplayOrder { get; set; }
public int? RevenueAccountId { get; set; }
public string? RevenueAccountName { get; set; }
public bool IsGiftCertificate { get; set; }
public int? GeneratedGiftCertificateId { get; set; }
public string? GeneratedGiftCertificateCode { get; set; }
}
public class CreateInvoiceItemDto
{
public int? SourceJobItemId { get; set; }
public int? CatalogItemId { get; set; }
public string Description { get; set; } = string.Empty;
public decimal Quantity { get; set; } = 1;
public decimal UnitPrice { get; set; }
public decimal TotalPrice { get; set; }
public string? ColorName { get; set; }
public string? Notes { get; set; }
public int DisplayOrder { get; set; }
public int? RevenueAccountId { get; set; }
public bool IsGiftCertificate { get; set; } = false;
public string? GcRecipientName { get; set; }
public string? GcRecipientEmail { get; set; }
public DateTime? GcExpiryDate { get; set; }
}
@@ -0,0 +1,52 @@
using PowderCoating.Core.Enums;
namespace PowderCoating.Application.DTOs.Invoice;
public class PaymentDtos
{
public class PaymentDto
{
public int Id { get; set; }
public int InvoiceId { get; set; }
public decimal Amount { get; set; }
public DateTime PaymentDate { get; set; }
public PaymentMethod PaymentMethod { get; set; }
public string PaymentMethodDisplay => PaymentMethod switch
{
PaymentMethod.Cash => "Cash",
PaymentMethod.Check => "Check",
PaymentMethod.CreditDebitCard => "Credit/Debit Card",
PaymentMethod.BankTransferACH => "Bank Transfer / ACH",
PaymentMethod.DigitalPayment => "Digital Payment",
_ => PaymentMethod.ToString()
};
public string? Reference { get; set; }
public string? Notes { get; set; }
public string? RecordedById { get; set; }
public string? RecordedByName { get; set; }
public int? DepositAccountId { get; set; }
public string? DepositAccountName { get; set; }
}
public class RecordPaymentDto
{
public int InvoiceId { get; set; }
public decimal Amount { get; set; }
public DateTime PaymentDate { get; set; } = DateTime.Today;
public PaymentMethod PaymentMethod { get; set; }
public string? Reference { get; set; }
public string? Notes { get; set; }
public int? DepositAccountId { get; set; }
}
public class EditPaymentDto
{
public int PaymentId { get; set; }
public int InvoiceId { get; set; }
public DateTime PaymentDate { get; set; }
public PaymentMethod PaymentMethod { get; set; }
public string? Reference { get; set; }
public string? Notes { get; set; }
public int? DepositAccountId { get; set; }
}
}
@@ -0,0 +1,92 @@
using PowderCoating.Core.Enums;
namespace PowderCoating.Application.DTOs.Invoice;
// ── Refund DTOs ───────────────────────────────────────────────────────────────
public class RefundDto
{
public int Id { get; set; }
public int InvoiceId { get; set; }
public int? PaymentId { get; set; }
public decimal Amount { get; set; }
public DateTime RefundDate { get; set; }
public PaymentMethod RefundMethod { get; set; }
public string RefundMethodDisplay => RefundMethod switch
{
PaymentMethod.Cash => "Cash",
PaymentMethod.Check => "Check",
PaymentMethod.CreditDebitCard => "Credit/Debit Card",
PaymentMethod.BankTransferACH => "Bank Transfer / ACH",
PaymentMethod.DigitalPayment => "Digital Payment",
PaymentMethod.StoreCredit => "Store Credit",
_ => RefundMethod.ToString()
};
public string Reason { get; set; } = string.Empty;
public string? Reference { get; set; }
public string? Notes { get; set; }
public RefundStatus Status { get; set; }
public DateTime? IssuedDate { get; set; }
public string? IssuedByName { get; set; }
}
public class IssueRefundDto
{
public int? PaymentId { get; set; }
public decimal Amount { get; set; }
public DateTime RefundDate { get; set; } = DateTime.Today;
public PaymentMethod RefundMethod { get; set; }
public string Reason { get; set; } = string.Empty;
public string? Reference { get; set; }
public string? Notes { get; set; }
}
// ── Credit Memo DTOs ──────────────────────────────────────────────────────────
public class CreditMemoDto
{
public int Id { get; set; }
public string MemoNumber { get; set; } = string.Empty;
public int CustomerId { get; set; }
public string CustomerName { get; set; } = string.Empty;
public int? OriginalInvoiceId { get; set; }
public string? OriginalInvoiceNumber { get; set; }
public int? ReworkRecordId { get; set; }
public decimal Amount { get; set; }
public decimal AmountApplied { get; set; }
public decimal RemainingBalance { get; set; }
public DateTime IssueDate { get; set; }
public DateTime? ExpiryDate { get; set; }
public string Reason { get; set; } = string.Empty;
public string? Notes { get; set; }
public CreditMemoStatus Status { get; set; }
public string? IssuedByName { get; set; }
public List<CreditMemoApplicationDto> Applications { get; set; } = new();
}
public class CreditMemoApplicationDto
{
public int Id { get; set; }
public int CreditMemoId { get; set; }
public string MemoNumber { get; set; } = string.Empty;
public int InvoiceId { get; set; }
public string InvoiceNumber { get; set; } = string.Empty;
public decimal AmountApplied { get; set; }
public DateTime AppliedDate { get; set; }
public string? AppliedByName { get; set; }
}
public class IssueCreditMemoDto
{
public decimal Amount { get; set; }
public string Reason { get; set; } = string.Empty;
public string? Notes { get; set; }
public DateTime? ExpiryDate { get; set; }
public int? ReworkRecordId { get; set; }
}
public class ApplyCreditDto
{
public int CreditMemoId { get; set; }
public decimal Amount { get; set; }
}
@@ -0,0 +1,13 @@
namespace PowderCoating.Application.DTOs.Job;
public class JobChangeHistoryDto
{
public int Id { get; set; }
public int JobId { get; set; }
public DateTime ChangedAt { get; set; }
public string ChangedByName { get; set; } = string.Empty;
public string FieldName { get; set; } = string.Empty;
public string? OldValue { get; set; }
public string? NewValue { get; set; }
public string ChangeDescription { get; set; } = string.Empty;
}
@@ -0,0 +1,45 @@
namespace PowderCoating.Application.DTOs.Job;
public class JobItemSummaryDto
{
public string Description { get; set; } = string.Empty;
public decimal Quantity { get; set; }
public List<string> Colors { get; set; } = new();
public bool HasSandblasting { get; set; }
public bool HasMasking { get; set; }
}
public class JobDailyPriorityDto
{
public int Id { get; set; }
public int JobId { get; set; }
public string JobNumber { get; set; } = string.Empty;
public string CustomerName { get; set; } = string.Empty;
public string StatusDisplayName { get; set; } = string.Empty;
public string StatusColorClass { get; set; } = string.Empty;
public int JobPriorityId { get; set; }
public string PriorityDisplayName { get; set; } = string.Empty;
public string PriorityColorClass { get; set; } = string.Empty;
public string? AssignedUserId { get; set; }
public string? AssignedWorkerName { get; set; }
public DateTime? ScheduledDate { get; set; }
public DateTime? DueDate { get; set; }
public int DisplayOrder { get; set; }
// New fields for enriched shop floor display
public string Description { get; set; } = string.Empty;
public int ItemCount { get; set; }
public int TotalPieces { get; set; }
public bool HasSandblasting { get; set; }
public bool HasMasking { get; set; }
public string? SpecialInstructions { get; set; }
public List<string> Colors { get; set; } = new();
public List<JobItemSummaryDto> Items { get; set; } = new();
public int StatusDisplayOrder { get; set; }
public string StatusCode { get; set; } = string.Empty;
public bool StatusIsTerminal { get; set; }
public int? NextStatusId { get; set; }
public string? NextStatusDisplayName { get; set; }
public string? NextStatusColorClass { get; set; }
public bool IntakeCompleted { get; set; }
}
@@ -0,0 +1,517 @@
using System.ComponentModel.DataAnnotations;
using PowderCoating.Application.DTOs.PrepService;
using PowderCoating.Application.DTOs.Quote;
namespace PowderCoating.Application.DTOs.Job;
public class JobDto
{
public int Id { get; set; }
public string JobNumber { get; set; } = string.Empty;
public int CustomerId { get; set; }
public string CustomerName { get; set; } = string.Empty;
public string? CustomerCompanyName { get; set; }
public string? CustomerContactName { get; set; }
public int? QuoteId { get; set; }
public string? QuoteNumber { get; set; }
public string? AssignedUserId { get; set; }
public string? AssignedWorkerName { get; set; }
public string Description { get; set; } = string.Empty;
// Job Status (from lookup table)
public int JobStatusId { get; set; }
public string StatusCode { get; set; } = string.Empty; // For code logic
public string StatusDisplayName { get; set; } = string.Empty; // For UI
public string StatusColorClass { get; set; } = "secondary"; // For badges
public string? StatusIconClass { get; set; } // For icons
public int StatusDisplayOrder { get; set; } // For comparisons
public bool StatusIsTerminal { get; set; } // For filtering
public bool StatusIsWIP { get; set; } // For stats
// Job Priority (from lookup table)
public int JobPriorityId { get; set; }
public string PriorityCode { get; set; } = string.Empty;
public string PriorityDisplayName { get; set; } = string.Empty;
public string PriorityColorClass { get; set; } = "secondary";
public string? PriorityIconClass { get; set; }
public int PriorityDisplayOrder { get; set; }
public DateTime? ScheduledDate { get; set; }
public DateTime? DueDate { get; set; }
public DateTime? CompletedDate { get; set; }
// Oven selection (carried over from quote)
public int? OvenCostId { get; set; }
public string? OvenLabel { get; set; }
public decimal QuotedPrice { get; set; }
public decimal FinalPrice { get; set; }
public string? CustomerPO { get; set; }
public string? SpecialInstructions { get; set; }
public string? InternalNotes { get; set; }
public string? Tags { get; set; }
public bool RequiresCustomerApproval { get; set; }
public bool IsCustomerApproved { get; set; }
// Job Completion Details
public decimal? ActualTimeSpentHours { get; set; }
// Part intake / receiving
public DateTime? IntakeDate { get; set; }
public string? IntakeConditionNotes { get; set; }
public int? IntakePartCount { get; set; }
public string? IntakeCheckedByUserId { get; set; }
public string? IntakeCheckedByName { get; set; }
// Time tracking
public List<JobTimeEntryDto> TimeEntries { get; set; } = new();
public decimal TotalLoggedHours => TimeEntries.Sum(t => t.HoursWorked);
// Rework
public bool IsReworkJob { get; set; }
public int? OriginalJobId { get; set; }
public string? OriginalJobNumber { get; set; }
public List<JobItemDto> Items { get; set; } = new();
public List<PrepServiceDto> PrepServices { get; set; } = new();
public List<int> PrepServiceIds { get; set; } = new();
public DateTime CreatedAt { get; set; }
}
public class JobListDto
{
public int Id { get; set; }
public string JobNumber { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string CustomerName { get; set; } = string.Empty;
public string? AssignedUserId { get; set; }
public string? AssignedWorkerName { get; set; }
// Job Status (from lookup table)
public int JobStatusId { get; set; }
public string StatusCode { get; set; } = string.Empty;
public string StatusDisplayName { get; set; } = string.Empty;
public string StatusColorClass { get; set; } = "secondary";
public bool StatusIsWIP { get; set; }
// Job Priority (from lookup table)
public int JobPriorityId { get; set; }
public string PriorityCode { get; set; } = string.Empty;
public string PriorityDisplayName { get; set; } = string.Empty;
public string PriorityColorClass { get; set; } = "secondary";
public DateTime? ScheduledDate { get; set; }
public DateTime? DueDate { get; set; }
public decimal FinalPrice { get; set; }
public DateTime CreatedAt { get; set; }
public string? Tags { get; set; }
public bool IsReworkJob { get; set; }
public int? OriginalJobId { get; set; }
}
public class CreateJobDto
{
[Required(ErrorMessage = "Customer is required")]
[Display(Name = "Customer")]
public int CustomerId { get; set; }
[Display(Name = "Quote")]
public int? QuoteId { get; set; }
[Display(Name = "Assigned Worker")]
public string? AssignedUserId { get; set; }
[Display(Name = "Oven")]
public int? OvenCostId { get; set; }
[Required(ErrorMessage = "Description is required")]
[StringLength(2000, ErrorMessage = "Description cannot exceed 2000 characters")]
[Display(Name = "Description")]
public string Description { get; set; } = string.Empty;
[Required(ErrorMessage = "Priority is required")]
[Display(Name = "Priority")]
public int JobPriorityId { get; set; } // FK to lookup table
[Display(Name = "Scheduled Date")]
public DateTime? ScheduledDate { get; set; }
[Display(Name = "Due Date")]
public DateTime? DueDate { get; set; }
[Range(0, 9999999.99, ErrorMessage = "Quoted price must be between 0 and 9,999,999.99")]
[Display(Name = "Quoted Price")]
public decimal QuotedPrice { get; set; }
[StringLength(100, ErrorMessage = "Customer PO cannot exceed 100 characters")]
[Display(Name = "Customer PO")]
public string? CustomerPO { get; set; }
[StringLength(2000, ErrorMessage = "Special instructions cannot exceed 2000 characters")]
[Display(Name = "Special Instructions")]
public string? SpecialInstructions { get; set; }
[StringLength(2000, ErrorMessage = "Internal notes cannot exceed 2000 characters")]
[Display(Name = "Internal Notes")]
public string? InternalNotes { get; set; }
[Display(Name = "Tags")]
[StringLength(500)]
public string? Tags { get; set; }
[Display(Name = "Requires Customer Approval")]
public bool RequiresCustomerApproval { get; set; }
[Display(Name = "Rush Job")]
public bool IsRushJob { get; set; }
[Display(Name = "Discount Type")]
public string DiscountType { get; set; } = "None";
[Range(0, double.MaxValue, ErrorMessage = "Discount value must be 0 or greater")]
[Display(Name = "Discount Value")]
public decimal DiscountValue { get; set; }
[StringLength(500)]
[Display(Name = "Discount Reason")]
public string? DiscountReason { get; set; }
public List<CreateQuoteItemDto> JobItems { get; set; } = new();
public List<int> PrepServiceIds { get; set; } = new();
}
public class UpdateJobDto
{
[Required]
public int Id { get; set; }
[Required(ErrorMessage = "Customer is required")]
[Display(Name = "Customer")]
public int CustomerId { get; set; }
[Display(Name = "Quote")]
public int? QuoteId { get; set; }
[Display(Name = "Assigned Worker")]
public string? AssignedUserId { get; set; }
[Required(ErrorMessage = "Description is required")]
[StringLength(2000, ErrorMessage = "Description cannot exceed 2000 characters")]
[Display(Name = "Description")]
public string Description { get; set; } = string.Empty;
[Required(ErrorMessage = "Status is required")]
[Display(Name = "Status")]
public int JobStatusId { get; set; } // FK to lookup table
[Required(ErrorMessage = "Priority is required")]
[Display(Name = "Priority")]
public int JobPriorityId { get; set; } // FK to lookup table
[Display(Name = "Scheduled Date")]
public DateTime? ScheduledDate { get; set; }
[Display(Name = "Due Date")]
public DateTime? DueDate { get; set; }
[Range(0, 9999999.99, ErrorMessage = "Quoted price must be between 0 and 9,999,999.99")]
[Display(Name = "Quoted Price")]
public decimal QuotedPrice { get; set; }
[StringLength(100, ErrorMessage = "Customer PO cannot exceed 100 characters")]
[Display(Name = "Customer PO")]
public string? CustomerPO { get; set; }
[StringLength(2000, ErrorMessage = "Special instructions cannot exceed 2000 characters")]
[Display(Name = "Special Instructions")]
public string? SpecialInstructions { get; set; }
[StringLength(2000, ErrorMessage = "Internal notes cannot exceed 2000 characters")]
[Display(Name = "Internal Notes")]
public string? InternalNotes { get; set; }
[Display(Name = "Tags")]
[StringLength(500)]
public string? Tags { get; set; }
[Display(Name = "Requires Customer Approval")]
public bool RequiresCustomerApproval { get; set; }
[Display(Name = "Rush Job")]
public bool IsRushJob { get; set; }
[Display(Name = "Discount Type")]
public string DiscountType { get; set; } = "None";
[Range(0, double.MaxValue, ErrorMessage = "Discount value must be 0 or greater")]
[Display(Name = "Discount Value")]
public decimal DiscountValue { get; set; }
[StringLength(500)]
[Display(Name = "Discount Reason")]
public string? DiscountReason { get; set; }
public List<CreateQuoteItemDto> JobItems { get; set; } = new();
public List<int> PrepServiceIds { get; set; } = new();
[Display(Name = "Notify customer of status change via email")]
public bool SendEmailOnStatusChange { get; set; } = false;
}
public class UpdateJobItemDto
{
public int Id { get; set; }
public string Description { get; set; } = string.Empty;
public decimal Quantity { get; set; }
public string? ColorName { get; set; }
public string? ColorCode { get; set; }
public decimal? SurfaceArea { get; set; }
public decimal UnitPrice { get; set; }
public decimal TotalPrice { get; set; }
public bool RequiresSandblasting { get; set; }
public bool RequiresMasking { get; set; }
public int EstimatedMinutes { get; set; }
public string? Notes { get; set; }
}
public class JobItemDto
{
public int Id { get; set; }
public string Description { get; set; } = string.Empty;
public decimal Quantity { get; set; }
public string? ColorName { get; set; }
public string? ColorCode { get; set; }
public string? Finish { get; set; }
public decimal? SurfaceArea { get; set; }
public decimal SurfaceAreaSqFt { get; set; }
public int EstimatedMinutes { get; set; }
public decimal UnitPrice { get; set; }
public decimal TotalPrice { get; set; }
public decimal LaborCost { get; set; }
public bool RequiresSandblasting { get; set; }
public bool RequiresMasking { get; set; }
public string? Notes { get; set; }
public int? CatalogItemId { get; set; }
public bool IsGenericItem { get; set; }
public bool IsLaborItem { get; set; }
public bool IsSalesItem { get; set; }
public string? Sku { get; set; }
public List<JobItemCoatDto> Coats { get; set; } = new();
public List<JobItemPrepServiceDto> PrepServices { get; set; } = new();
}
public class JobItemPrepServiceDto
{
public int PrepServiceId { get; set; }
public string? PrepServiceName { get; set; }
public int EstimatedMinutes { get; set; }
/// <summary>Blast setup selected in wizard for this sandblasting prep service.</summary>
public int? BlastSetupId { get; set; }
}
public class CreateJobItemDto
{
public string Description { get; set; } = string.Empty;
public decimal Quantity { get; set; }
public string? ColorName { get; set; }
public string? ColorCode { get; set; }
public string? Finish { get; set; }
public decimal UnitPrice { get; set; }
public bool RequiresSandblasting { get; set; }
public bool RequiresMasking { get; set; }
public int EstimatedMinutes { get; set; }
public string? Notes { get; set; }
}
// DTO for Shop Floor Display
public class ShopFloorJobDto
{
public int Id { get; set; }
public string JobNumber { get; set; } = string.Empty;
public string CustomerName { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
// Job Status (from lookup table)
public int JobStatusId { get; set; }
public string StatusCode { get; set; } = string.Empty;
public string StatusDisplayName { get; set; } = string.Empty;
public string StatusColorClass { get; set; } = "secondary";
// Job Priority (from lookup table)
public int JobPriorityId { get; set; }
public string PriorityCode { get; set; } = string.Empty;
public string PriorityDisplayName { get; set; } = string.Empty;
public string PriorityColorClass { get; set; } = "secondary";
public string? AssignedWorkerName { get; set; }
public DateTime? ScheduledDate { get; set; }
public DateTime? DueDate { get; set; }
public int ItemCount { get; set; }
public List<string> NextSteps { get; set; } = new();
}
// DTO for Job Item Coat (multi-coat support)
public class JobItemCoatDto
{
public int Id { get; set; }
public int JobItemId { get; set; }
public string CoatName { get; set; } = string.Empty;
public int Sequence { get; set; }
public int? InventoryItemId { get; set; }
public string? ColorName { get; set; }
public int? VendorId { get; set; }
public string? VendorName { get; set; }
public string? ColorCode { get; set; }
public string? Finish { get; set; }
public decimal CoverageSqFtPerLb { get; set; }
public decimal TransferEfficiency { get; set; }
public decimal? PowderCostPerLb { get; set; }
public decimal? PowderToOrder { get; set; }
public decimal? ActualPowderUsedLbs { get; set; } // Filled during job completion
public string? Notes { get; set; }
}
// DTO for completing a job
public class CompleteJobDto
{
public int JobId { get; set; }
public decimal? ActualTimeSpentHours { get; set; }
public List<JobItemCoatUsageDto> CoatUsages { get; set; } = new();
public bool SendEmailToCustomer { get; set; } = false;
}
// DTO for tracking actual powder usage per coat
public class JobItemCoatUsageDto
{
public int JobItemCoatId { get; set; }
public decimal? ActualPowderUsedLbs { get; set; }
}
// ── Time Tracking DTOs ────────────────────────────────────────────────────────
public class JobTimeEntryDto
{
public int Id { get; set; }
public int JobId { get; set; }
public int ShopWorkerId { get; set; }
public string WorkerName { get; set; } = string.Empty;
public string WorkerRole { get; set; } = string.Empty;
public DateTime WorkDate { get; set; }
public decimal HoursWorked { get; set; }
public string? Stage { get; set; }
public string? Notes { get; set; }
public DateTime CreatedAt { get; set; }
}
public class CreateJobTimeEntryDto
{
public int JobId { get; set; }
public int ShopWorkerId { get; set; }
public DateTime WorkDate { get; set; }
public decimal HoursWorked { get; set; }
public string? Stage { get; set; }
public string? Notes { get; set; }
}
public class UpdateJobTimeEntryDto
{
public int Id { get; set; }
public int ShopWorkerId { get; set; }
public DateTime WorkDate { get; set; }
public decimal HoursWorked { get; set; }
public string? Stage { get; set; }
public string? Notes { get; set; }
}
// ── Rework / Warranty DTOs ────────────────────────────────────────────────────
public class ReworkRecordDto
{
public int Id { get; set; }
public int JobId { get; set; }
public int? JobItemId { get; set; }
public string? JobItemDescription { get; set; }
public int? ReworkJobId { get; set; }
public string? ReworkJobNumber { get; set; }
public PowderCoating.Core.Enums.ReworkType ReworkType { get; set; }
public string ReworkTypeDisplay { get; set; } = string.Empty;
public PowderCoating.Core.Enums.ReworkReason Reason { get; set; }
public string ReasonDisplay { get; set; } = string.Empty;
public string DefectDescription { get; set; } = string.Empty;
public PowderCoating.Core.Enums.ReworkDiscoveredBy DiscoveredBy { get; set; }
public string DiscoveredByDisplay { get; set; } = string.Empty;
public DateTime DiscoveredDate { get; set; }
public string? ReportedByName { get; set; }
public decimal EstimatedReworkCost { get; set; }
public decimal ActualReworkCost { get; set; }
public bool IsBillableToCustomer { get; set; }
public string? BillingNotes { get; set; }
public PowderCoating.Core.Enums.ReworkStatus Status { get; set; }
public string StatusDisplay { get; set; } = string.Empty;
public string StatusColorClass { get; set; } = "secondary";
public PowderCoating.Core.Enums.ReworkResolution? Resolution { get; set; }
public string? ResolutionDisplay { get; set; }
public DateTime? ResolvedDate { get; set; }
public string? ResolutionNotes { get; set; }
public DateTime CreatedAt { get; set; }
}
public class CreateReworkRecordDto
{
public int JobId { get; set; }
public int? JobItemId { get; set; }
public PowderCoating.Core.Enums.ReworkType ReworkType { get; set; }
public PowderCoating.Core.Enums.ReworkReason Reason { get; set; }
public string DefectDescription { get; set; } = string.Empty;
public PowderCoating.Core.Enums.ReworkDiscoveredBy DiscoveredBy { get; set; }
public DateTime DiscoveredDate { get; set; } = DateTime.Today;
public string? ReportedByName { get; set; }
public decimal EstimatedReworkCost { get; set; }
public bool IsBillableToCustomer { get; set; }
public string? BillingNotes { get; set; }
}
public class UpdateReworkRecordDto
{
public int Id { get; set; }
public PowderCoating.Core.Enums.ReworkStatus Status { get; set; }
public PowderCoating.Core.Enums.ReworkResolution? Resolution { get; set; }
public decimal ActualReworkCost { get; set; }
public bool IsBillableToCustomer { get; set; }
public string? BillingNotes { get; set; }
public DateTime? ResolvedDate { get; set; }
public string? ResolutionNotes { get; set; }
public int? ReworkJobId { get; set; }
}
// ViewModel for the Edit Items wizard page
public class JobEditItemsViewModel
{
public int JobId { get; set; }
public string JobNumber { get; set; } = string.Empty;
public int? CustomerId { get; set; }
public decimal TaxPercent { get; set; }
public List<CreateQuoteItemDto> JobItems { get; set; } = new();
}
// DTO for the part intake / receiving form
public class IntakeJobDto
{
[Required]
public int JobId { get; set; }
[Display(Name = "Actual Part Count")]
[Range(0, 10000, ErrorMessage = "Part count must be between 0 and 10,000")]
public int? ActualPartCount { get; set; }
[StringLength(2000, ErrorMessage = "Condition notes cannot exceed 2000 characters")]
[Display(Name = "Condition Notes")]
public string? ConditionNotes { get; set; }
[Display(Name = "Advance status to In Preparation")]
public bool AdvanceToInPreparation { get; set; } = true;
}
@@ -0,0 +1,56 @@
using PowderCoating.Core.Enums;
namespace PowderCoating.Application.DTOs.Job;
public class JobPhotoDto
{
public int Id { get; set; }
public int JobId { get; set; }
public string FilePath { get; set; } = string.Empty;
public string FileName { get; set; } = string.Empty;
public long FileSize { get; set; }
public string ContentType { get; set; } = string.Empty;
public string? Caption { get; set; }
public JobPhotoType PhotoType { get; set; }
public string PhotoTypeDisplay { get; set; } = string.Empty;
public int DisplayOrder { get; set; }
public string UploadedById { get; set; } = string.Empty;
public string UploadedByName { get; set; } = string.Empty;
public DateTime UploadedDate { get; set; }
public string? Tags { get; set; }
// Helper properties
public string FileSizeDisplay => FormatFileSize(FileSize);
public List<string> TagsList => string.IsNullOrWhiteSpace(Tags)
? new List<string>()
: Tags.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
private static string FormatFileSize(long bytes)
{
string[] sizes = { "B", "KB", "MB", "GB" };
double len = bytes;
int order = 0;
while (len >= 1024 && order < sizes.Length - 1)
{
order++;
len = len / 1024;
}
return $"{len:0.##} {sizes[order]}";
}
}
public class UploadJobPhotoDto
{
public int JobId { get; set; }
public string? Caption { get; set; }
public JobPhotoType PhotoType { get; set; } = JobPhotoType.Progress;
}
public class UpdateJobPhotoDto
{
public int Id { get; set; }
public string? Caption { get; set; }
public JobPhotoType PhotoType { get; set; }
public int DisplayOrder { get; set; }
public string? Tags { get; set; }
}
@@ -0,0 +1,7 @@
namespace PowderCoating.Application.DTOs.Job;
public class UpdateJobOrderDto
{
public int JobId { get; set; }
public int DisplayOrder { get; set; }
}
@@ -0,0 +1,65 @@
using System.ComponentModel.DataAnnotations;
namespace PowderCoating.Application.DTOs.Lookup;
public class AppointmentTypeLookupDto
{
public int Id { get; set; }
public string TypeCode { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public int DisplayOrder { get; set; }
public string ColorClass { get; set; } = "primary";
public string? IconClass { get; set; }
public bool RequiresJobLink { get; set; }
public bool IsActive { get; set; }
public bool IsSystemDefined { get; set; }
public string? Description { get; set; }
public int AppointmentCount { get; set; } // Number of appointments using this type
}
public class CreateAppointmentTypeLookupDto
{
[Required, MaxLength(50)]
public string TypeCode { get; set; } = string.Empty;
[Required, MaxLength(100)]
public string DisplayName { get; set; } = string.Empty;
[Range(1, 999)]
public int DisplayOrder { get; set; }
[Required, MaxLength(50)]
public string ColorClass { get; set; } = "primary";
[MaxLength(50)]
public string? IconClass { get; set; }
[MaxLength(500)]
public string? Description { get; set; }
public bool RequiresJobLink { get; set; }
}
public class UpdateAppointmentTypeLookupDto
{
public int Id { get; set; }
[Required, MaxLength(100)]
public string DisplayName { get; set; } = string.Empty; // TypeCode NOT editable
[Range(1, 999)]
public int DisplayOrder { get; set; }
[Required, MaxLength(50)]
public string ColorClass { get; set; } = "primary";
[MaxLength(50)]
public string? IconClass { get; set; }
public bool IsActive { get; set; }
[MaxLength(500)]
public string? Description { get; set; }
public bool RequiresJobLink { get; set; }
}
@@ -0,0 +1,64 @@
using System.ComponentModel.DataAnnotations;
namespace PowderCoating.Application.DTOs.Lookup;
public class InventoryCategoryLookupDto
{
public int Id { get; set; }
public string CategoryCode { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public int DisplayOrder { get; set; }
public string? Description { get; set; }
public bool IsActive { get; set; }
public bool IsSystemDefined { get; set; }
public bool IsCoating { get; set; }
public int ItemCount { get; set; } // Number of inventory items using this category
}
public class CreateInventoryCategoryLookupDto
{
[Required]
[MaxLength(50)]
[Display(Name = "Category Code")]
public string CategoryCode { get; set; } = string.Empty;
[Required]
[MaxLength(100)]
[Display(Name = "Display Name")]
public string DisplayName { get; set; } = string.Empty;
[Range(1, 999)]
[Display(Name = "Display Order")]
public int DisplayOrder { get; set; }
[Display(Name = "Is Coating")]
public bool IsCoating { get; set; } = false;
[MaxLength(500)]
[Display(Name = "Description")]
public string? Description { get; set; }
}
public class UpdateInventoryCategoryLookupDto
{
public int Id { get; set; }
[Required]
[MaxLength(100)]
[Display(Name = "Display Name")]
public string DisplayName { get; set; } = string.Empty; // CategoryCode NOT editable
[Range(1, 999)]
[Display(Name = "Display Order")]
public int DisplayOrder { get; set; }
[Display(Name = "Active")]
public bool IsActive { get; set; }
[Display(Name = "Is Coating")]
public bool IsCoating { get; set; }
[MaxLength(500)]
[Display(Name = "Description")]
public string? Description { get; set; }
}
@@ -0,0 +1,60 @@
using System.ComponentModel.DataAnnotations;
namespace PowderCoating.Application.DTOs.Lookup;
public class JobPriorityLookupDto
{
public int Id { get; set; }
public string PriorityCode { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public int DisplayOrder { get; set; }
public string ColorClass { get; set; } = "secondary";
public string? IconClass { get; set; }
public bool IsActive { get; set; }
public bool IsSystemDefined { get; set; }
public string? Description { get; set; }
public int JobCount { get; set; } // Number of jobs using this priority
}
public class CreateJobPriorityLookupDto
{
[Required, MaxLength(50)]
public string PriorityCode { get; set; } = string.Empty;
[Required, MaxLength(100)]
public string DisplayName { get; set; } = string.Empty;
[Range(1, 999)]
public int DisplayOrder { get; set; }
[RegularExpression("^(primary|secondary|success|danger|warning|info|dark)$")]
public string ColorClass { get; set; } = "secondary";
[MaxLength(50)]
public string? IconClass { get; set; }
[MaxLength(500)]
public string? Description { get; set; }
}
public class UpdateJobPriorityLookupDto
{
public int Id { get; set; }
[Required, MaxLength(100)]
public string DisplayName { get; set; } = string.Empty; // PriorityCode NOT editable
[Range(1, 999)]
public int DisplayOrder { get; set; }
[RegularExpression("^(primary|secondary|success|danger|warning|info|dark)$")]
public string ColorClass { get; set; } = "secondary";
[MaxLength(50)]
public string? IconClass { get; set; }
public bool IsActive { get; set; }
[MaxLength(500)]
public string? Description { get; set; }
}
@@ -0,0 +1,81 @@
using System.ComponentModel.DataAnnotations;
namespace PowderCoating.Application.DTOs.Lookup;
public class JobStatusLookupDto
{
public int Id { get; set; }
public string StatusCode { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public int DisplayOrder { get; set; }
public string ColorClass { get; set; } = "secondary";
public string? IconClass { get; set; }
public bool IsActive { get; set; }
public bool IsSystemDefined { get; set; }
public bool IsTerminalStatus { get; set; }
public bool IsWorkInProgressStatus { get; set; }
public string? Description { get; set; }
public string? WorkflowCategory { get; set; }
public int JobCount { get; set; } // Number of jobs using this status
}
public class CreateJobStatusLookupDto
{
[Required, MaxLength(50)]
public string StatusCode { get; set; } = string.Empty;
[Required, MaxLength(100)]
public string DisplayName { get; set; } = string.Empty;
[Range(1, 999)]
public int DisplayOrder { get; set; }
[RegularExpression("^(primary|secondary|success|danger|warning|info|dark)$")]
public string ColorClass { get; set; } = "secondary";
[MaxLength(50)]
public string? IconClass { get; set; }
[MaxLength(500)]
public string? Description { get; set; }
[MaxLength(100)]
public string? WorkflowCategory { get; set; }
public bool IsTerminalStatus { get; set; }
public bool IsWorkInProgressStatus { get; set; }
}
public class UpdateJobStatusLookupDto
{
public int Id { get; set; }
[Required, MaxLength(100)]
public string DisplayName { get; set; } = string.Empty; // StatusCode NOT editable
[Range(1, 999)]
public int DisplayOrder { get; set; }
[RegularExpression("^(primary|secondary|success|danger|warning|info|dark)$")]
public string ColorClass { get; set; } = "secondary";
[MaxLength(50)]
public string? IconClass { get; set; }
public bool IsActive { get; set; }
[MaxLength(500)]
public string? Description { get; set; }
[MaxLength(100)]
public string? WorkflowCategory { get; set; }
public bool IsTerminalStatus { get; set; }
public bool IsWorkInProgressStatus { get; set; }
}
public class ReorderLookupDto
{
[Required]
public List<int> OrderedIds { get; set; } = new();
}
@@ -0,0 +1,71 @@
using System.ComponentModel.DataAnnotations;
namespace PowderCoating.Application.DTOs.Lookup;
public class QuoteStatusLookupDto
{
public int Id { get; set; }
public string StatusCode { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public int DisplayOrder { get; set; }
public string ColorClass { get; set; } = "secondary";
public string? IconClass { get; set; }
public bool IsActive { get; set; }
public bool IsSystemDefined { get; set; }
public bool IsApprovedStatus { get; set; }
public bool IsConvertedStatus { get; set; }
public bool IsDraftStatus { get; set; }
public string? Description { get; set; }
public int QuoteCount { get; set; } // Number of quotes using this status
}
public class CreateQuoteStatusLookupDto
{
[Required, MaxLength(50)]
public string StatusCode { get; set; } = string.Empty;
[Required, MaxLength(100)]
public string DisplayName { get; set; } = string.Empty;
[Range(1, 999)]
public int DisplayOrder { get; set; }
[RegularExpression("^(primary|secondary|success|danger|warning|info|dark)$")]
public string ColorClass { get; set; } = "secondary";
[MaxLength(50)]
public string? IconClass { get; set; }
[MaxLength(500)]
public string? Description { get; set; }
public bool IsApprovedStatus { get; set; }
public bool IsConvertedStatus { get; set; }
public bool IsDraftStatus { get; set; }
}
public class UpdateQuoteStatusLookupDto
{
public int Id { get; set; }
[Required, MaxLength(100)]
public string DisplayName { get; set; } = string.Empty; // StatusCode NOT editable
[Range(1, 999)]
public int DisplayOrder { get; set; }
[RegularExpression("^(primary|secondary|success|danger|warning|info|dark)$")]
public string ColorClass { get; set; } = "secondary";
[MaxLength(50)]
public string? IconClass { get; set; }
public bool IsActive { get; set; }
[MaxLength(500)]
public string? Description { get; set; }
public bool IsApprovedStatus { get; set; }
public bool IsConvertedStatus { get; set; }
public bool IsDraftStatus { get; set; }
}
@@ -0,0 +1,146 @@
using System.ComponentModel.DataAnnotations;
using PowderCoating.Core.Enums;
namespace PowderCoating.Application.DTOs.Maintenance;
public class MaintenanceRecordDto
{
public int Id { get; set; }
public int EquipmentId { get; set; }
public string EquipmentName { get; set; } = string.Empty;
public string MaintenanceType { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public string StatusDisplay { get; set; } = string.Empty;
public string Priority { get; set; } = string.Empty;
public string PriorityDisplay { get; set; } = string.Empty;
public DateTime ScheduledDate { get; set; }
public DateTime? CompletedDate { get; set; }
public string? PerformedById { get; set; }
public string? PerformedByName { get; set; }
public string? AssignedUserId { get; set; }
public string? AssignedWorkerName { get; set; }
public string Description { get; set; } = string.Empty;
public string? WorkPerformed { get; set; }
public string? PartsReplaced { get; set; }
public decimal LaborCost { get; set; }
public decimal PartsCost { get; set; }
public decimal TotalCost { get; set; }
public decimal DowntimeHours { get; set; }
public string? Notes { get; set; }
public string? TechnicianNotes { get; set; }
// Recurrence
public bool IsRecurring { get; set; }
public string? RecurrenceFrequency { get; set; } // display string, e.g. "Weekly"
public DateTime? RecurrenceEndDate { get; set; }
public string? RecurrenceGroupId { get; set; }
public int? RecurrenceParentId { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
}
public class MaintenanceListDto
{
public int Id { get; set; }
public int EquipmentId { get; set; }
public string EquipmentName { get; set; } = string.Empty;
public string MaintenanceType { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public string StatusDisplay { get; set; } = string.Empty;
public string Priority { get; set; } = string.Empty;
public string PriorityDisplay { get; set; } = string.Empty;
public DateTime ScheduledDate { get; set; }
public DateTime? CompletedDate { get; set; }
public decimal TotalCost { get; set; }
// Recurrence
public bool IsRecurring { get; set; }
public string? RecurrenceGroupId { get; set; }
}
public class CreateMaintenanceDto
{
[Required(ErrorMessage = "Equipment is required")]
[Display(Name = "Equipment")]
public int EquipmentId { get; set; }
[Required(ErrorMessage = "Maintenance type is required")]
[StringLength(100, ErrorMessage = "Maintenance type cannot exceed 100 characters")]
[Display(Name = "Maintenance Type")]
public string MaintenanceType { get; set; } = string.Empty;
[Required(ErrorMessage = "Status is required")]
[StringLength(50, ErrorMessage = "Status cannot exceed 50 characters")]
[Display(Name = "Status")]
public string Status { get; set; } = "Scheduled";
[Required(ErrorMessage = "Priority is required")]
[StringLength(50, ErrorMessage = "Priority cannot exceed 50 characters")]
[Display(Name = "Priority")]
public string Priority { get; set; } = "Normal";
[Required(ErrorMessage = "Scheduled date is required")]
[Display(Name = "Scheduled Date")]
public DateTime ScheduledDate { get; set; }
[Display(Name = "Completed Date")]
public DateTime? CompletedDate { get; set; }
[Display(Name = "Performed By")]
public string? PerformedById { get; set; }
[Required(ErrorMessage = "Description is required")]
[StringLength(2000, ErrorMessage = "Description cannot exceed 2000 characters")]
[Display(Name = "Description")]
public string Description { get; set; } = string.Empty;
[StringLength(2000, ErrorMessage = "Work performed cannot exceed 2000 characters")]
[Display(Name = "Work Performed")]
public string? WorkPerformed { get; set; }
[StringLength(2000, ErrorMessage = "Parts replaced cannot exceed 2000 characters")]
[Display(Name = "Parts Replaced")]
public string? PartsReplaced { get; set; }
[Range(0, 9999999.99, ErrorMessage = "Labor cost must be between 0 and 9,999,999.99")]
[Display(Name = "Labor Cost")]
public decimal LaborCost { get; set; }
[Range(0, 9999999.99, ErrorMessage = "Parts cost must be between 0 and 9,999,999.99")]
[Display(Name = "Parts Cost")]
public decimal PartsCost { get; set; }
[Range(0, 999999, ErrorMessage = "Downtime hours must be between 0 and 999,999")]
[Display(Name = "Downtime Hours")]
public decimal DowntimeHours { get; set; }
[StringLength(2000, ErrorMessage = "Notes cannot exceed 2000 characters")]
[Display(Name = "Notes")]
public string? Notes { get; set; }
[StringLength(2000, ErrorMessage = "Technician notes cannot exceed 2000 characters")]
[Display(Name = "Technician Notes")]
public string? TechnicianNotes { get; set; }
// Recurrence
[Display(Name = "Recurring Maintenance")]
public bool IsRecurring { get; set; }
[Display(Name = "Frequency")]
public MaintenanceRecurrenceFrequency? RecurrenceFrequency { get; set; }
[Display(Name = "Recurrence End Date")]
public DateTime? RecurrenceEndDate { get; set; }
}
public class UpdateMaintenanceDto : CreateMaintenanceDto
{
[Required]
public int Id { get; set; }
}
@@ -0,0 +1,41 @@
using PowderCoating.Core.Enums;
namespace PowderCoating.Application.DTOs.Notification;
public class NotificationLogDto
{
public int Id { get; set; }
public NotificationChannel Channel { get; set; }
public string ChannelDisplay => Channel == NotificationChannel.Email ? "Email" : "SMS";
public NotificationType NotificationType { get; set; }
public string NotificationTypeDisplay => NotificationType switch
{
NotificationType.QuoteSent => "Quote Sent",
NotificationType.QuoteApproved => "Quote Approved",
NotificationType.JobStatusChanged => "Job Status Changed",
NotificationType.JobReadyForPickup => "Ready for Pickup",
NotificationType.JobCompleted => "Job Completed",
NotificationType.SmsConsentConfirmation => "SMS Consent Confirmation",
_ => NotificationType.ToString()
};
public NotificationStatus Status { get; set; }
public string StatusDisplay => Status switch
{
NotificationStatus.Sent => "Sent",
NotificationStatus.Failed => "Failed",
NotificationStatus.Skipped => "Skipped",
_ => Status.ToString()
};
public string RecipientName { get; set; } = string.Empty;
public string Recipient { get; set; } = string.Empty;
public string? Subject { get; set; }
public string Message { get; set; } = string.Empty;
public string? ErrorMessage { get; set; }
public DateTime SentAt { get; set; }
public int? CustomerId { get; set; }
public int? JobId { get; set; }
public int? QuoteId { get; set; }
public string? JobNumber { get; set; }
public string? QuoteNumber { get; set; }
public string? CustomerName { get; set; }
}
@@ -0,0 +1,23 @@
using PowderCoating.Core.Enums;
using System.ComponentModel.DataAnnotations;
namespace PowderCoating.Application.DTOs.Notification;
public class NotificationTemplateDto
{
public int Id { get; set; }
public NotificationType NotificationType { get; set; }
public NotificationChannel Channel { get; set; }
public string DisplayName { get; set; } = string.Empty;
public string? Subject { get; set; }
public string Body { get; set; } = string.Empty;
public bool IsEmail => Channel == NotificationChannel.Email;
public DateTime? UpdatedAt { get; set; }
}
public class UpdateNotificationTemplateDto
{
public int Id { get; set; }
[StringLength(300)] public string? Subject { get; set; }
[Required] public string Body { get; set; } = string.Empty;
}
@@ -0,0 +1,128 @@
namespace PowderCoating.Application.DTOs.Powder;
/// <summary>Readiness state — controls which layers are shown in the UI.</summary>
public class PowderDataReadiness
{
public int JobsWithActualData { get; set; }
public int Layer3MinJobs { get; set; }
public int Layer2MinJobs { get; set; }
public bool IsLayer2Ready { get; set; }
public bool IsLayer3Ready { get; set; }
public int Layer3ProgressPercent { get; set; } // 0-100, for the progress bar
}
/// <summary>Per-job powder summary: estimated vs actual vs variance.</summary>
public class JobPowderSummaryDto
{
public int JobId { get; set; }
public string JobNumber { get; set; } = string.Empty;
public List<CoatPowderSummaryDto> Coats { get; set; } = new();
public decimal TotalEstimatedLbs { get; set; }
public decimal TotalActualLbs { get; set; }
public decimal TotalVarianceLbs { get; set; }
public bool HasAllActuals { get; set; }
}
public class CoatPowderSummaryDto
{
public int JobItemCoatId { get; set; }
public int JobItemId { get; set; }
public string ItemDescription { get; set; } = string.Empty;
public string CoatName { get; set; } = string.Empty;
public string? ColorName { get; set; }
public string? InventoryItemName { get; set; }
public decimal? EstimatedLbs { get; set; }
public decimal? ActualLbs { get; set; }
public decimal? VarianceLbs { get; set; }
public decimal? VariancePct { get; set; }
public bool IsRecorded { get; set; }
}
/// <summary>Layer 2: Low stock alert with job pipeline demand forecast.</summary>
public class LowStockForecastDto
{
public int InventoryItemId { get; set; }
public string Name { get; set; } = string.Empty;
public string? ColorName { get; set; }
public string? ColorCode { get; set; }
public string? Manufacturer { get; set; }
public decimal CurrentStockLbs { get; set; }
public decimal ScheduledDemandLbs { get; set; } // Sum of PowderToOrder on active jobs
public decimal ReorderPoint { get; set; }
public decimal ReorderQuantity { get; set; }
public decimal ShortfallLbs { get; set; } // Max(0, Demand - Stock)
public bool IsAtRisk { get; set; } // Stock < Demand
public bool IsBelowReorderPoint { get; set; } // Stock < ReorderPoint
public int ActiveJobCount { get; set; } // # of active jobs needing this powder
}
/// <summary>Layer 2: Per-SKU coverage efficiency — actual vs catalog spec.</summary>
public class PowderEfficiencyDto
{
public int InventoryItemId { get; set; }
public string Name { get; set; } = string.Empty;
public string? ColorName { get; set; }
public string? Manufacturer { get; set; }
public decimal CatalogCoverageSqFtPerLb { get; set; } // What the spec says
public decimal ActualAvgCoverageSqFtPerLb { get; set; } // What we're actually getting
public decimal VariancePct { get; set; } // (Actual - Catalog) / Catalog * 100
public int SampleCount { get; set; } // # of coats with actuals
public decimal TotalEstimatedLbs { get; set; }
public decimal TotalActualLbs { get; set; }
public bool IsBelowSpec { get; set; } // Actual < Catalog (wasting powder)
public bool HasEnoughData { get; set; } // SampleCount >= 5
}
/// <summary>Layer 3: Suggested reorder quantity based on pipeline + historical usage.</summary>
public class PowderReorderSuggestionDto
{
public int InventoryItemId { get; set; }
public string Name { get; set; } = string.Empty;
public string? ColorName { get; set; }
public string? Manufacturer { get; set; }
public decimal CurrentStockLbs { get; set; }
public decimal PipelineDemand30DaysLbs { get; set; } // Scheduled jobs next 30 days
public decimal HistoricalAvgMonthlyUsageLbs { get; set; } // Based on actual data
public decimal SuggestedOrderQtyLbs { get; set; } // Calculated suggestion
public decimal ConfiguredReorderQty { get; set; } // InventoryItem.ReorderQuantity
public int SampleJobCount { get; set; } // # of completed jobs used in calculation
public decimal ConfidenceScore { get; set; } // 0-1, based on sample size
}
/// <summary>Layer 3: Jobs where actual powder significantly exceeded estimate.</summary>
public class WastePatternDto
{
public int JobId { get; set; }
public string JobNumber { get; set; } = string.Empty;
public string ItemDescription { get; set; } = string.Empty;
public string CoatName { get; set; } = string.Empty;
public string? InventoryItemName { get; set; }
public string? Complexity { get; set; }
public decimal EstimatedLbs { get; set; }
public decimal ActualLbs { get; set; }
public decimal OveragePct { get; set; }
public DateTime JobDate { get; set; }
}
/// <summary>Dashboard view model combining all layers.</summary>
public class PowderInsightsDashboardDto
{
public PowderDataReadiness Readiness { get; set; } = new();
public List<LowStockForecastDto> LowStockAlerts { get; set; } = new(); // Layer 2
public List<PowderEfficiencyDto> EfficiencyBySku { get; set; } = new(); // Layer 2
public List<PowderReorderSuggestionDto> ReorderSuggestions { get; set; } = new(); // Layer 3
public List<WastePatternDto> WastePatterns { get; set; } = new(); // Layer 3
public int ActiveJobsNeedingPowder { get; set; }
public decimal TotalEstimatedPowderNeededLbs { get; set; }
}
/// <summary>Result returned from RecordUsage AJAX call.</summary>
public class RecordUsageResultDto
{
public bool Success { get; set; }
public string? ErrorMessage { get; set; }
public decimal ActualLbs { get; set; }
public decimal EstimatedLbs { get; set; }
public decimal VarianceLbs { get; set; }
public decimal VariancePct { get; set; }
}
@@ -0,0 +1,39 @@
namespace PowderCoating.Application.DTOs.PrepService;
/// <summary>
/// DTO for displaying a prep service in lists
/// </summary>
public class PrepServiceDto
{
public int Id { get; set; }
public string ServiceName { get; set; } = string.Empty;
public string? Description { get; set; }
public int DisplayOrder { get; set; }
public bool IsActive { get; set; }
public bool RequiresBlastSetup { get; set; }
}
/// <summary>
/// DTO for creating a new prep service
/// </summary>
public class CreatePrepServiceDto
{
public string ServiceName { get; set; } = string.Empty;
public string? Description { get; set; }
public int DisplayOrder { get; set; }
public bool IsActive { get; set; } = true;
public bool RequiresBlastSetup { get; set; }
}
/// <summary>
/// DTO for updating an existing prep service
/// </summary>
public class UpdatePrepServiceDto
{
public int Id { get; set; }
public string ServiceName { get; set; } = string.Empty;
public string? Description { get; set; }
public int DisplayOrder { get; set; }
public bool IsActive { get; set; }
public bool RequiresBlastSetup { get; set; }
}
@@ -0,0 +1,196 @@
using System.ComponentModel.DataAnnotations;
using PowderCoating.Core.Enums;
namespace PowderCoating.Application.DTOs.PurchaseOrder;
// ============================================================================
// LIST / SUMMARY
// ============================================================================
public class PurchaseOrderListDto
{
public int Id { get; set; }
public string PoNumber { get; set; } = string.Empty;
public int VendorId { get; set; }
public string VendorName { get; set; } = string.Empty;
public PurchaseOrderStatus Status { get; set; }
public DateTime OrderDate { get; set; }
public DateTime? ExpectedDeliveryDate { get; set; }
public DateTime? ReceivedDate { get; set; }
public decimal ShippingCost { get; set; }
public decimal SubTotal { get; set; }
public decimal TotalAmount { get; set; }
public int ItemCount { get; set; }
public bool IsOverdue =>
Status is PurchaseOrderStatus.Draft or PurchaseOrderStatus.Submitted or PurchaseOrderStatus.PartiallyReceived
&& ExpectedDeliveryDate.HasValue
&& ExpectedDeliveryDate.Value.Date < DateTime.UtcNow.Date;
}
// ============================================================================
// DETAILS
// ============================================================================
public class PurchaseOrderDto
{
public int Id { get; set; }
public string PoNumber { get; set; } = string.Empty;
public int VendorId { get; set; }
public string VendorName { get; set; } = string.Empty;
public string? VendorEmail { get; set; }
public string? VendorPhone { get; set; }
public PurchaseOrderStatus Status { get; set; }
public DateTime OrderDate { get; set; }
public DateTime? ExpectedDeliveryDate { get; set; }
public DateTime? ReceivedDate { get; set; }
public decimal ShippingCost { get; set; }
public decimal SubTotal { get; set; }
public decimal TotalAmount { get; set; }
public string? Notes { get; set; }
public string? InternalNotes { get; set; }
public int? BillId { get; set; }
public string? BillNumber { get; set; }
public List<PurchaseOrderItemDto> Items { get; set; } = new();
public bool IsOverdue =>
Status is PurchaseOrderStatus.Draft or PurchaseOrderStatus.Submitted or PurchaseOrderStatus.PartiallyReceived
&& ExpectedDeliveryDate.HasValue
&& ExpectedDeliveryDate.Value.Date < DateTime.UtcNow.Date;
}
// ============================================================================
// CREATE / UPDATE
// ============================================================================
public class CreatePurchaseOrderDto
{
[Required]
public int VendorId { get; set; }
[Required]
[Display(Name = "Order Date")]
public DateTime OrderDate { get; set; } = DateTime.Today;
[Display(Name = "Expected Delivery")]
public DateTime? ExpectedDeliveryDate { get; set; }
[Display(Name = "Shipping Cost")]
[Range(0, 999999)]
public decimal ShippingCost { get; set; } = 0;
[Display(Name = "Notes")]
[StringLength(1000)]
public string? Notes { get; set; }
[Display(Name = "Internal Notes")]
[StringLength(1000)]
public string? InternalNotes { get; set; }
public List<CreatePurchaseOrderItemDto> Items { get; set; } = new();
}
public class UpdatePurchaseOrderDto
{
[Required]
public int VendorId { get; set; }
[Required]
[Display(Name = "Order Date")]
public DateTime OrderDate { get; set; } = DateTime.Today;
[Display(Name = "Expected Delivery")]
public DateTime? ExpectedDeliveryDate { get; set; }
[Display(Name = "Shipping Cost")]
[Range(0, 999999)]
public decimal ShippingCost { get; set; } = 0;
[Display(Name = "Notes")]
[StringLength(1000)]
public string? Notes { get; set; }
[Display(Name = "Internal Notes")]
[StringLength(1000)]
public string? InternalNotes { get; set; }
public List<CreatePurchaseOrderItemDto> Items { get; set; } = new();
}
// ============================================================================
// LINE ITEMS
// ============================================================================
public class PurchaseOrderItemDto
{
public int Id { get; set; }
public int PurchaseOrderId { get; set; }
public int? InventoryItemId { get; set; }
public string ItemName { get; set; } = string.Empty; // Inventory item name OR custom description
public string ItemSKU { get; set; } = string.Empty;
public string UnitOfMeasure { get; set; } = string.Empty;
public bool IsCustomItem => InventoryItemId == null;
public decimal QuantityOrdered { get; set; }
public decimal QuantityReceived { get; set; }
public decimal UnitCost { get; set; }
public decimal LineTotal { get; set; }
public string? Notes { get; set; }
public bool IsFullyReceived => QuantityReceived >= QuantityOrdered;
public decimal QuantityRemaining => Math.Max(0, QuantityOrdered - QuantityReceived);
}
public class CreatePurchaseOrderItemDto
{
// Null = custom/non-inventory line item
public int? InventoryItemId { get; set; }
// Required when InventoryItemId is null
[StringLength(200)]
public string? Description { get; set; }
[StringLength(50)]
public string? UnitOfMeasure { get; set; }
[Required]
[Range(0.001, 999999, ErrorMessage = "Quantity must be greater than 0")]
public decimal QuantityOrdered { get; set; }
[Required]
[Range(0, 999999)]
public decimal UnitCost { get; set; }
[StringLength(500)]
public string? Notes { get; set; }
}
// ============================================================================
// RECEIVING
// ============================================================================
public class ReceivePurchaseOrderDto
{
[Required]
[Display(Name = "Received Date")]
public DateTime ReceivedDate { get; set; } = DateTime.Today;
[StringLength(500)]
public string? Notes { get; set; }
public List<ReceiveItemDto> Items { get; set; } = new();
}
public class ReceiveItemDto
{
public int PurchaseOrderItemId { get; set; }
public int? InventoryItemId { get; set; }
public string ItemName { get; set; } = string.Empty;
public string ItemSKU { get; set; } = string.Empty;
public string UnitOfMeasure { get; set; } = string.Empty;
public decimal QuantityOrdered { get; set; }
public decimal QuantityAlreadyReceived { get; set; }
public decimal QuantityRemaining { get; set; }
[Range(0, 999999)]
public decimal QuantityToReceive { get; set; }
}
@@ -0,0 +1,22 @@
namespace PowderCoating.Application.DTOs.QuickBooks;
/// <summary>
/// A single entry in an import log — covers errors, warnings, and skipped records.
/// </summary>
public class ImportErrorDto
{
/// <summary>"Error", "Warning", or "Skipped"</summary>
public string Severity { get; set; } = "Error";
public int LineNumber { get; set; }
public string? RecordName { get; set; }
public string? FieldName { get; set; }
public string ErrorMessage { get; set; } = string.Empty;
public string DisplayMessage =>
$"[{Severity}]" +
(LineNumber > 0 ? $" Line {LineNumber}" : "") +
(string.IsNullOrEmpty(RecordName) ? "" : $" ({RecordName})") +
(string.IsNullOrEmpty(FieldName) ? "" : $" — {FieldName}") +
$": {ErrorMessage}";
}
@@ -0,0 +1,23 @@
namespace PowderCoating.Application.DTOs.QuickBooks;
/// <summary>
/// Result of an import operation from QuickBooks IIF file.
/// </summary>
public class ImportResultDto
{
public bool Success { get; set; }
public int TotalRecords { get; set; }
public int ImportedCount { get; set; }
public int UpdatedCount { get; set; }
public int SkippedCount { get; set; }
/// <summary>
/// Records that were intentionally not re-imported because they already exist
/// in the destination (positive outcome — not an error or warning).
/// Displayed to the user separately from SkippedCount to avoid confusion.
/// </summary>
public int AlreadyRecordedCount { get; set; }
public List<ImportErrorDto> Errors { get; set; } = new();
public string Summary =>
$"{ImportedCount} imported, {UpdatedCount} updated, {SkippedCount} skipped out of {TotalRecords} total records.";
}
@@ -0,0 +1,10 @@
namespace PowderCoating.Application.DTOs.QuickBooks;
/// <summary>
/// Result of validating an IIF file before import.
/// </summary>
public class ValidationResultDto
{
public bool IsValid { get; set; }
public List<ImportErrorDto> Errors { get; set; } = new();
}
@@ -0,0 +1,13 @@
namespace PowderCoating.Application.DTOs.Quote;
public class QuoteChangeHistoryDto
{
public int Id { get; set; }
public int QuoteId { get; set; }
public DateTime ChangedAt { get; set; }
public string ChangedByName { get; set; } = string.Empty;
public string FieldName { get; set; } = string.Empty;
public string? OldValue { get; set; }
public string? NewValue { get; set; }
public string ChangeDescription { get; set; } = string.Empty;
}
@@ -0,0 +1,832 @@
using System.ComponentModel.DataAnnotations;
using PowderCoating.Application.DTOs.PrepService;
namespace PowderCoating.Application.DTOs.Quote;
// ============================================================================
// LIST DTO - For Index page listing
// ============================================================================
public class QuoteListDto
{
public int Id { get; set; }
public string QuoteNumber { get; set; } = string.Empty;
public string CustomerOrProspectName { get; set; } = string.Empty;
// Quote Status (from lookup table)
public int QuoteStatusId { get; set; }
public string StatusCode { get; set; } = string.Empty;
public string StatusDisplayName { get; set; } = string.Empty;
public string StatusColorClass { get; set; } = "secondary";
public bool StatusIsDraft { get; set; }
public DateTime QuoteDate { get; set; }
public DateTime? ExpirationDate { get; set; }
public decimal Total { get; set; }
public string? Notes { get; set; }
public string? Description { get; set; }
public string? Tags { get; set; }
public bool IsExpired => ExpirationDate.HasValue && ExpirationDate.Value < DateTime.UtcNow && StatusIsDraft;
public bool IsProspect { get; set; }
}
// ============================================================================
// FULL QUOTE DTO - For Details page
// ============================================================================
public class QuoteDto
{
public int Id { get; set; }
public string QuoteNumber { get; set; } = string.Empty;
// Customer or Prospect Info
public int? CustomerId { get; set; }
public string? CustomerName { get; set; }
public string? CustomerCompanyName { get; set; }
public string? CustomerContactFirstName { get; set; }
public string? CustomerContactLastName { get; set; }
public string? CustomerEmail { get; set; }
public string? CustomerPhone { get; set; }
public string? ProspectCompanyName { get; set; }
public string? ProspectContactFirstName { get; set; }
public string? ProspectContactLastName { get; set; }
public string? ProspectContactName { get; set; }
public string? ProspectEmail { get; set; }
public string? ProspectPhone { get; set; }
public string? ProspectAddress { get; set; }
public string? ProspectCity { get; set; }
public string? ProspectState { get; set; }
public string? ProspectZipCode { get; set; }
public string? PreparedById { get; set; }
public string? PreparedByName { get; set; }
public string? PreparedByEmail { get; set; }
// Quote Status (from lookup table)
public int QuoteStatusId { get; set; }
public string StatusCode { get; set; } = string.Empty;
public string StatusDisplayName { get; set; } = string.Empty;
public string StatusColorClass { get; set; } = "secondary";
public string? StatusIconClass { get; set; }
public bool StatusIsApproved { get; set; }
public bool StatusIsConverted { get; set; }
public bool StatusIsDraft { get; set; }
public bool IsCommercial { get; set; }
// Dates
public DateTime QuoteDate { get; set; }
public DateTime? ExpirationDate { get; set; }
public DateTime? SentDate { get; set; }
public DateTime? ApprovedDate { get; set; }
// Pricing
public decimal SubTotal { get; set; }
// Discount Information
public string DiscountType { get; set; } = "None"; // None, Percentage, FixedAmount
public decimal DiscountValue { get; set; }
public decimal DiscountPercent { get; set; }
public decimal DiscountAmount { get; set; }
public string? DiscountReason { get; set; }
public bool HideDiscountFromCustomer { get; set; }
public decimal TaxPercent { get; set; }
public decimal TaxAmount { get; set; }
public decimal RushFee { get; set; }
public decimal Total { get; set; }
public bool IsRushJob { get; set; }
// Additional Information
public string? Description { get; set; }
public string? Terms { get; set; }
public string? Notes { get; set; }
public string? CustomerPO { get; set; }
public string? Tags { get; set; }
// Items
public List<QuoteItemDto> QuoteItems { get; set; } = new();
// Prep Services
public List<PrepServiceDto> PrepServices { get; set; } = new();
public List<int> PrepServiceIds { get; set; } = new();
// Pricing Breakdown
public QuotePricingBreakdownDto? PricingBreakdown { get; set; }
// Oven selection
public int? OvenCostId { get; set; }
public string? OvenLabel { get; set; }
// Oven batch pricing
public int OvenBatches { get; set; } = 1;
public int? OvenCycleMinutes { get; set; }
// Conversion Tracking
public int? ConvertedToJobId { get; set; }
// Customer Approval Tracking
public string? ApprovalToken { get; set; }
public DateTime? ApprovalTokenExpiresAt { get; set; }
public DateTime? ApprovalTokenUsedAt { get; set; }
public string? DeclineReason { get; set; }
public bool IsProspect => !CustomerId.HasValue;
public bool IsExpired => ExpirationDate.HasValue && ExpirationDate.Value < DateTime.UtcNow && StatusIsDraft;
public string CustomerOrProspectName => CustomerName ?? ProspectCompanyName ?? ProspectContactName ?? "Unknown";
}
// ============================================================================
// CREATE QUOTE DTO - For creating new quotes
// ============================================================================
public class CreateQuoteDto
{
// Customer or Prospect Selection
public bool IsForProspect { get; set; }
[Display(Name = "Customer")]
public int? CustomerId { get; set; }
// Prospect Contact Information
[Display(Name = "Company Name")]
[StringLength(100)]
public string? ProspectCompanyName { get; set; }
[Display(Name = "Contact Name")]
[StringLength(100)]
public string? ProspectContactName { get; set; }
[Display(Name = "Email")]
[EmailAddress]
[StringLength(100)]
public string? ProspectEmail { get; set; }
[Display(Name = "Phone")]
[Phone]
[StringLength(20)]
public string? ProspectPhone { get; set; }
[Display(Name = "Address")]
[StringLength(200)]
public string? ProspectAddress { get; set; }
[Display(Name = "City")]
[StringLength(100)]
public string? ProspectCity { get; set; }
[Display(Name = "State")]
[StringLength(2)]
public string? ProspectState { get; set; }
[Display(Name = "Zip Code")]
[StringLength(10)]
public string? ProspectZipCode { get; set; }
// Oven Selection
[Display(Name = "Oven")]
public int? OvenCostId { get; set; }
// Oven Batch Pricing
[Display(Name = "Oven Batches")]
[Range(1, 9999)]
public int OvenBatches { get; set; } = 1;
[Display(Name = "Oven Cycle Time (min)")]
[Range(1, 1440)]
public int? OvenCycleMinutes { get; set; }
// Quote Information
[Display(Name = "Commercial Quote")]
public bool IsCommercial { get; set; }
[Display(Name = "Rush Job")]
public bool IsRushJob { get; set; }
[Display(Name = "Quote Date")]
[DataType(DataType.Date)]
public DateTime QuoteDate { get; set; } = DateTime.Today;
[Display(Name = "Expiration Date")]
[DataType(DataType.Date)]
public DateTime? ExpirationDate { get; set; }
[Display(Name = "Description")]
[StringLength(500)]
public string? Description { get; set; }
[Display(Name = "Terms & Conditions")]
[DataType(DataType.MultilineText)]
public string? Terms { get; set; }
[Display(Name = "Internal Notes")]
[DataType(DataType.MultilineText)]
public string? Notes { get; set; }
[Display(Name = "Customer PO Number")]
[StringLength(50)]
public string? CustomerPO { get; set; }
[Display(Name = "Tags")]
[StringLength(500)]
public string? Tags { get; set; }
// Pricing
[Display(Name = "Tax Percent (%)")]
[Range(0, 100)]
public decimal TaxPercent { get; set; }
// Discount
[Display(Name = "Discount Type")]
public string DiscountType { get; set; } = "None"; // None, Percentage, FixedAmount
[Display(Name = "Discount Value")]
[Range(0, 999999)]
public decimal DiscountValue { get; set; }
[Display(Name = "Discount Reason")]
[StringLength(200)]
public string? DiscountReason { get; set; }
[Display(Name = "Hide discount from customer")]
public bool HideDiscountFromCustomer { get; set; } = false;
// Items
[Required]
// Note: MinLength validation removed to prevent false positives on initial page load
// JavaScript validation handles this check on form submission
public List<CreateQuoteItemDto> QuoteItems { get; set; } = new();
// Prep Services
[Display(Name = "Preparation Services")]
public List<int> PrepServiceIds { get; set; } = new();
// Email Options
[Display(Name = "Send quote via email")]
public bool SendEmailToCustomer { get; set; } = false;
// AI Photo TempIds — list of uploaded temp photo IDs to promote on save
public List<string> AiPhotoTempIds { get; set; } = new();
// Quote Photo TempIds — general (non-AI) photos staged on the Create page
public List<string> QuotePhotoTempIds { get; set; } = new();
public List<string> QuotePhotoFileNames { get; set; } = new();
}
// ============================================================================
// UPDATE QUOTE DTO - For editing existing quotes
// ============================================================================
public class UpdateQuoteDto
{
public int Id { get; set; }
// Customer or Prospect Info (read-only after creation, but included for completeness)
public int? CustomerId { get; set; }
public bool IsForProspect { get; set; }
// Prospect Contact Information
[Display(Name = "Company Name")]
[StringLength(100)]
public string? ProspectCompanyName { get; set; }
[Display(Name = "Contact Name")]
[StringLength(100)]
public string? ProspectContactName { get; set; }
[Display(Name = "Email")]
[EmailAddress]
[StringLength(100)]
public string? ProspectEmail { get; set; }
[Display(Name = "Phone")]
[Phone]
[StringLength(20)]
public string? ProspectPhone { get; set; }
[Display(Name = "Address")]
[StringLength(200)]
public string? ProspectAddress { get; set; }
[Display(Name = "City")]
[StringLength(100)]
public string? ProspectCity { get; set; }
[Display(Name = "State")]
[StringLength(2)]
public string? ProspectState { get; set; }
[Display(Name = "Zip Code")]
[StringLength(10)]
public string? ProspectZipCode { get; set; }
// Oven Selection
[Display(Name = "Oven")]
public int? OvenCostId { get; set; }
// Oven Batch Pricing
[Display(Name = "Oven Batches")]
[Range(1, 9999)]
public int OvenBatches { get; set; } = 1;
[Display(Name = "Oven Cycle Time (min)")]
[Range(1, 1440)]
public int? OvenCycleMinutes { get; set; }
// Quote Information
[Display(Name = "Status")]
public int QuoteStatusId { get; set; } // FK to lookup table
[Display(Name = "Commercial Quote")]
public bool IsCommercial { get; set; }
[Display(Name = "Rush Job")]
public bool IsRushJob { get; set; }
[Display(Name = "Quote Date")]
[DataType(DataType.Date)]
public DateTime QuoteDate { get; set; }
[Display(Name = "Expiration Date")]
[DataType(DataType.Date)]
public DateTime? ExpirationDate { get; set; }
[Display(Name = "Description")]
[StringLength(500)]
public string? Description { get; set; }
[Display(Name = "Terms & Conditions")]
[DataType(DataType.MultilineText)]
public string? Terms { get; set; }
[Display(Name = "Internal Notes")]
[DataType(DataType.MultilineText)]
public string? Notes { get; set; }
[Display(Name = "Customer PO Number")]
[StringLength(50)]
public string? CustomerPO { get; set; }
[Display(Name = "Tags")]
[StringLength(500)]
public string? Tags { get; set; }
// Pricing
[Display(Name = "Tax Percent (%)")]
[Range(0, 100)]
public decimal TaxPercent { get; set; }
// Discount
[Display(Name = "Discount Type")]
public string DiscountType { get; set; } = "None"; // None, Percentage, FixedAmount
[Display(Name = "Discount Value")]
[Range(0, 999999)]
public decimal DiscountValue { get; set; }
[Display(Name = "Discount Reason")]
[StringLength(200)]
public string? DiscountReason { get; set; }
[Display(Name = "Hide discount from customer")]
public bool HideDiscountFromCustomer { get; set; } = false;
// Items
[Required]
// Note: MinLength validation removed to prevent false positives on initial page load
// JavaScript validation handles this check on form submission
public List<CreateQuoteItemDto> QuoteItems { get; set; } = new();
// Prep Services
[Display(Name = "Preparation Services")]
public List<int> PrepServiceIds { get; set; } = new();
// AI Photo TempIds — list of uploaded temp photo IDs to promote on save
public List<string> AiPhotoTempIds { get; set; } = new();
// Quote Photo TempIds — general (non-AI) photos staged on the Edit page (not used by direct-upload Edit path,
// kept for symmetry / future use)
public List<string> QuotePhotoTempIds { get; set; } = new();
public List<string> QuotePhotoFileNames { get; set; } = new();
}
// ============================================================================
// QUOTE ITEM DTO - For displaying quote items
// ============================================================================
public class QuoteItemDto
{
public int Id { get; set; }
public int QuoteId { get; set; }
[Display(Name = "Description")]
public string Description { get; set; } = string.Empty;
[Display(Name = "Quantity")]
public decimal Quantity { get; set; }
[Display(Name = "Surface Area (sq ft)")]
public decimal SurfaceAreaSqFt { get; set; }
[Display(Name = "Estimated Minutes")]
public int EstimatedMinutes { get; set; }
[Display(Name = "Requires Sandblasting")]
public bool RequiresSandblasting { get; set; }
[Display(Name = "Requires Masking")]
public bool RequiresMasking { get; set; }
[Display(Name = "Catalog Item")]
public int? CatalogItemId { get; set; }
public string? CatalogItemName { get; set; }
[Display(Name = "Unit Price")]
public decimal UnitPrice { get; set; }
[Display(Name = "Total Price")]
public decimal TotalPrice { get; set; }
public string? Notes { get; set; }
public bool IsGenericItem { get; set; }
public bool IsLaborItem { get; set; }
public bool IsSalesItem { get; set; }
public string? Sku { get; set; }
public decimal? ManualUnitPrice { get; set; }
// Coating layers
public List<QuoteItemCoatDto> Coats { get; set; } = new();
// Preparation services
public List<CreateQuoteItemPrepServiceDto> PrepServices { get; set; } = new();
// Part complexity level for calculated items
public string? Complexity { get; set; }
public bool IsAiItem { get; set; }
// Cost breakdown snapshot
public decimal ItemMaterialCost { get; set; }
public decimal ItemLaborCost { get; set; }
public decimal ItemEquipmentCost { get; set; }
}
// ============================================================================
// CREATE QUOTE ITEM DTO - For creating quote items
// ============================================================================
public class CreateQuoteItemDto
{
// Description is required for calculated items but auto-populated for catalog items
[Display(Name = "Description")]
[StringLength(200)]
public string Description { get; set; } = string.Empty;
[Required]
[Display(Name = "Quantity")]
[Range(0.01, 10000)]
public decimal Quantity { get; set; } = 1;
[Required]
[Display(Name = "Surface Area (sq ft)")]
[Range(0, 100000)] // 0 is valid for generic items
public decimal SurfaceAreaSqFt { get; set; }
[Required]
[Display(Name = "Estimated Minutes")]
[Range(0, 10000, ErrorMessage = "Estimated minutes must be between 0 and 10,000 (0 for catalog items)")]
public int EstimatedMinutes { get; set; }
[Display(Name = "Requires Sandblasting")]
public bool RequiresSandblasting { get; set; }
[Display(Name = "Requires Masking")]
public bool RequiresMasking { get; set; }
[Display(Name = "Catalog Item")]
public int? CatalogItemId { get; set; }
[Display(Name = "Notes")]
[StringLength(500)]
public string? Notes { get; set; }
// Generic item — price entered manually, bypasses calculation engine
public bool IsGenericItem { get; set; }
[Range(0, 1000000)]
public decimal? ManualUnitPrice { get; set; }
// Labor item — Quantity = hours; priced using StandardLaborRate × markup, no coats
public bool IsLaborItem { get; set; }
// Sales item — off-the-shelf merchandise (T-shirts, tumblers, etc.)
public bool IsSalesItem { get; set; }
public string? Sku { get; set; }
// Coating layers
public List<CreateQuoteItemCoatDto> Coats { get; set; } = new();
// Prep services (each has its own time estimate)
// For catalog items, prep cost is only included when IncludePrepCost = true
public List<CreateQuoteItemPrepServiceDto> PrepServices { get; set; } = new();
// Controls whether prep service labor cost is added to the item price.
// Always true for calculated items; catalog items default to false (costs baked in)
// but can be opted in via the wizard toggle.
public bool IncludePrepCost { get; set; } = true;
// Part complexity — drives a price multiplier on calculated items only
// Values: null | "Simple" | "Moderate" | "Complex" | "Extreme"
public string? Complexity { get; set; }
// Optional unit price override for catalog items — overrides the catalog default price
public decimal? PowderCostOverride { get; set; }
// True when this item was generated via AI photo analysis
public bool IsAiItem { get; set; }
// AI-generated standardized tags (comma-separated, e.g. "automotive,tubular")
public string? AiTags { get; set; }
// ID of the AiItemPrediction record captured at analysis time (null for non-AI items)
public int? AiPredictionId { get; set; }
}
// ============================================================================
// QUOTE ITEM PREP SERVICE DTO
// ============================================================================
public class CreateQuoteItemPrepServiceDto
{
[Required]
public int PrepServiceId { get; set; }
[Required]
[Range(0, 10000)]
public int EstimatedMinutes { get; set; }
/// <summary>Blast setup selected in wizard for this sandblasting prep service.</summary>
public int? BlastSetupId { get; set; }
// Populated when mapping from entity (for display purposes)
public string? PrepServiceName { get; set; }
}
// ============================================================================
// PRICING BREAKDOWN DTO - For transparent cost breakdown
// ============================================================================
public class QuotePricingBreakdownDto
{
// Per-Item Costs
public decimal MaterialCosts { get; set; }
public decimal LaborCosts { get; set; }
public decimal EquipmentCosts { get; set; }
public decimal ItemSubtotal { get; set; }
// Quote-Level Costs
public decimal ItemsSubtotal { get; set; }
public decimal ShopSuppliesAmount { get; set; }
public decimal ShopSuppliesPercent { get; set; }
public decimal OverheadCosts { get; set; }
public decimal OverheadPercent { get; set; }
public decimal ProfitMargin { get; set; }
public decimal ProfitPercent { get; set; }
public decimal SubtotalBeforeDiscount { get; set; }
public decimal DiscountAmount { get; set; }
public decimal DiscountPercent { get; set; }
public decimal RushFee { get; set; }
public decimal SubtotalAfterDiscount { get; set; }
public decimal TaxAmount { get; set; }
public decimal TaxPercent { get; set; }
public decimal OvenBatchCost { get; set; }
public int OvenBatches { get; set; }
public int OvenCycleMinutes { get; set; }
public decimal Total { get; set; }
// Cost Breakdown Details
public string CostBreakdownDetails { get; set; } = string.Empty;
}
// ============================================================================
// CONVERT TO CUSTOMER DTO - For prospect-to-customer conversion
// ============================================================================
public class ConvertQuoteToCustomerDto
{
public int QuoteId { get; set; }
public string QuoteNumber { get; set; } = string.Empty;
// Pre-filled from prospect data
[Display(Name = "Company Name")]
[StringLength(100)]
public string? CompanyName { get; set; }
[Required]
[Display(Name = "Contact Name")]
[StringLength(100)]
public string ContactName { get; set; } = string.Empty;
[Display(Name = "Email")]
[EmailAddress]
[StringLength(100)]
public string? Email { get; set; }
[Display(Name = "Phone")]
[Phone]
[StringLength(20)]
public string? Phone { get; set; }
[Display(Name = "Address")]
[StringLength(200)]
public string? Address { get; set; }
[Display(Name = "City")]
[StringLength(100)]
public string? City { get; set; }
[Display(Name = "State")]
[StringLength(2)]
public string? State { get; set; }
[Display(Name = "Zip Code")]
[StringLength(10)]
public string? ZipCode { get; set; }
// Additional customer fields
[Display(Name = "Tax ID / EIN")]
[StringLength(50)]
public string? TaxId { get; set; }
[Display(Name = "Credit Limit")]
[Range(0, 1000000)]
public decimal CreditLimit { get; set; }
[Display(Name = "Pricing Tier")]
public int? PricingTierId { get; set; }
[Display(Name = "Customer Type")]
public bool IsCommercial { get; set; } = true;
[Display(Name = "Payment Terms")]
[StringLength(100)]
public string? PaymentTerms { get; set; }
[Display(Name = "Notes")]
[DataType(DataType.MultilineText)]
public string? Notes { get; set; }
}
// ============================================================================
// QUOTE ITEM COAT DTOs - For multi-coat support
// ============================================================================
/// <summary>
/// DTO for creating/editing a coating layer on a quote item
/// </summary>
public class CreateQuoteItemCoatDto
{
[Required]
[StringLength(100)]
[Display(Name = "Coat Name")]
public string CoatName { get; set; } = "Base Coat";
public int Sequence { get; set; } = 1;
[Display(Name = "Inventory Powder")]
public int? InventoryItemId { get; set; }
[StringLength(100)]
[Display(Name = "Color Name")]
public string? ColorName { get; set; }
[Display(Name = "Vendor")]
public int? VendorId { get; set; }
[StringLength(50)]
[Display(Name = "Color Code")]
public string? ColorCode { get; set; }
[StringLength(50)]
[Display(Name = "Finish")]
public string? Finish { get; set; }
[Range(1, 500)]
[Display(Name = "Coverage (sq ft/lb)")]
public decimal CoverageSqFtPerLb { get; set; } = 30m;
[Range(1, 100)]
[Display(Name = "Transfer Efficiency (%)")]
public decimal TransferEfficiency { get; set; } = 65m;
[Range(0, 1000)]
[Display(Name = "Powder Cost ($/lb)")]
public decimal? PowderCostPerLb { get; set; }
[Range(0, 10000)]
[Display(Name = "Powder to Order (lbs)")]
public decimal? PowderToOrder { get; set; }
[StringLength(500)]
[Display(Name = "Notes")]
public string? Notes { get; set; }
/// <summary>
/// When true, the additional layer labor charge is not applied even if this is not the first coat.
/// </summary>
public bool NoExtraLayerCharge { get; set; }
}
/// <summary>
/// DTO for displaying a coating layer on a quote item
/// </summary>
public class QuoteItemCoatDto
{
public int Id { get; set; }
public string CoatName { get; set; } = string.Empty;
public int Sequence { get; set; }
public int? InventoryItemId { get; set; }
public string? InventoryItemName { get; set; }
public int? VendorId { get; set; }
public string? VendorName { get; set; }
public string? ColorName { get; set; }
public string? ColorCode { get; set; }
public string? Finish { get; set; }
public decimal CoverageSqFtPerLb { get; set; }
public decimal TransferEfficiency { get; set; }
public decimal? PowderToOrder { get; set; }
public decimal CoatMaterialCost { get; set; }
public decimal CoatLaborCost { get; set; }
public decimal CoatTotalCost { get; set; }
public string? Notes { get; set; }
}
// ============================================================================
// PRICING CALCULATION RESULT - Internal use for pricing service
// ============================================================================
/// <summary>
/// Result of calculating costs for a single coating layer
/// </summary>
public class QuoteItemCoatPricingResult
{
public decimal CoatMaterialCost { get; set; }
public decimal CoatLaborCost { get; set; }
public decimal CoatTotalCost { get; set; }
}
public class QuoteItemPricingResult
{
public decimal MaterialCost { get; set; }
public decimal LaborCost { get; set; }
public decimal EquipmentCost { get; set; }
public decimal ItemSubtotal { get; set; }
public decimal UnitPrice { get; set; }
public decimal TotalPrice { get; set; }
}
public class QuotePricingResult
{
public decimal ItemsSubtotal { get; set; }
public decimal ShopSuppliesAmount { get; set; }
public decimal ShopSuppliesPercent { get; set; }
public decimal OverheadCosts { get; set; }
public decimal OverheadPercent { get; set; }
public decimal ProfitMargin { get; set; }
public decimal ProfitPercent { get; set; }
public decimal SubtotalBeforeDiscount { get; set; }
// Pricing Tier Discount (from customer's pricing tier)
public decimal PricingTierDiscountAmount { get; set; }
public decimal PricingTierDiscountPercent { get; set; }
// Quote-Level Discount (applied on the quote)
public decimal QuoteDiscountAmount { get; set; }
public decimal QuoteDiscountPercent { get; set; }
// Combined Discount (for backward compatibility)
public decimal DiscountAmount { get; set; }
public decimal DiscountPercent { get; set; }
public decimal SubtotalAfterDiscount { get; set; }
public decimal RushFee { get; set; }
public decimal TaxAmount { get; set; }
public decimal TaxPercent { get; set; }
public decimal Total { get; set; }
// Oven batch cost (quote-level, separate from per-item calculations)
public decimal OvenBatchCost { get; set; }
public int OvenBatches { get; set; }
public int OvenCycleMinutes { get; set; }
// Detailed breakdown for transparency
public decimal MaterialCosts { get; set; }
public decimal LaborCosts { get; set; }
public decimal EquipmentCosts { get; set; }
// Per-item results (same order as input items)
public List<QuoteItemPricingResult> ItemResults { get; set; } = new();
}
@@ -0,0 +1,26 @@
namespace PowderCoating.Application.DTOs.Quote;
public class QuotePhotoDto
{
public int Id { get; set; }
public int? QuoteId { get; set; }
public string TempId { get; set; } = string.Empty;
public string FilePath { get; set; } = string.Empty;
public string FileName { get; set; } = string.Empty;
public long FileSize { get; set; }
public string ContentType { get; set; } = string.Empty;
public string? Caption { get; set; }
public bool IsAiAnalysisPhoto { get; set; }
public string? UploadedById { get; set; }
public string? UploadedByName { get; set; }
public DateTime UploadedDate { get; set; }
public string FileSizeDisplay => FormatFileSize(FileSize);
private static string FormatFileSize(long bytes)
{
string[] sizes = { "B", "KB", "MB", "GB" };
double len = bytes;
int order = 0;
while (len >= 1024 && order < sizes.Length - 1) { order++; len /= 1024; }
return $"{len:0.##} {sizes[order]}";
}
}
@@ -0,0 +1,34 @@
using System.ComponentModel.DataAnnotations;
namespace PowderCoating.Application.DTOs.Registration;
public class RegisterCompanyDto
{
public int Plan { get; set; } = 0;
public bool IsAnnual { get; set; } = false;
[Required(ErrorMessage = "Company name is required")]
[StringLength(200, ErrorMessage = "Company name cannot exceed 200 characters")]
[Display(Name = "Company Name")]
public string CompanyName { get; set; } = string.Empty;
[Phone(ErrorMessage = "Please enter a valid phone number")]
[Display(Name = "Company Phone")]
public string? CompanyPhone { get; set; }
[Required(ErrorMessage = "First name is required")]
[StringLength(100)]
[Display(Name = "First Name")]
public string FirstName { get; set; } = string.Empty;
[Required(ErrorMessage = "Last name is required")]
[StringLength(100)]
[Display(Name = "Last Name")]
public string LastName { get; set; } = string.Empty;
[Required(ErrorMessage = "Email is required")]
[EmailAddress(ErrorMessage = "Please enter a valid email address")]
[Display(Name = "Email")]
public string Email { get; set; } = string.Empty;
}
@@ -0,0 +1,201 @@
namespace PowderCoating.Application.DTOs.Scheduling;
// ──────────────────────────────────────────────────────────────────────────────
// Request
// ──────────────────────────────────────────────────────────────────────────────
public class BatchSchedulingRequest
{
public DateTime ScheduleFromDate { get; set; } = DateTime.Today;
public string OptimizationGoal { get; set; } = "maximize_throughput";
// maximize_throughput | minimize_lateness | minimize_color_changes
public List<OvenConfigDto> Ovens { get; set; } = new();
public List<OvenReadyJobDto> Jobs { get; set; } = new();
}
public class OvenConfigDto
{
public int OvenCostId { get; set; }
public int EquipmentId { get; set; }
public string Name { get; set; } = string.Empty;
public decimal? MaxLoadSqFt { get; set; }
public int CycleMinutes { get; set; } = 45;
public decimal? MinTempF { get; set; }
public decimal? MaxTempF { get; set; }
}
public class OvenReadyJobDto
{
public int JobId { get; set; }
public string JobNumber { get; set; } = string.Empty;
public string CustomerName { get; set; } = string.Empty;
public string Priority { get; set; } = "Normal";
public DateTime? DueDate { get; set; }
public string Status { get; set; } = string.Empty;
public List<OvenReadyItemDto> Items { get; set; } = new();
}
public class OvenReadyItemDto
{
public int JobItemId { get; set; }
public string Description { get; set; } = string.Empty;
public decimal Quantity { get; set; }
public decimal SurfaceAreaSqFt { get; set; }
public decimal TotalSqFt { get; set; }
public List<OvenReadyCoatDto> Coats { get; set; } = new();
}
public class OvenReadyCoatDto
{
public int JobItemCoatId { get; set; }
public string CoatName { get; set; } = string.Empty;
public int Sequence { get; set; }
public string? ColorName { get; set; }
public string? ColorCode { get; set; }
public decimal? CureTemperatureF { get; set; }
public int? CureTimeMinutes { get; set; }
public bool AlreadyBaked { get; set; }
}
// ──────────────────────────────────────────────────────────────────────────────
// Response
// ──────────────────────────────────────────────────────────────────────────────
public class BatchScheduleSuggestion
{
public bool Success { get; set; }
public string? ErrorMessage { get; set; }
public List<SuggestedBatch> Batches { get; set; } = new();
public string Summary { get; set; } = string.Empty;
public List<string> Warnings { get; set; } = new();
}
public class SuggestedBatch
{
public string BatchName { get; set; } = string.Empty;
public int OvenCostId { get; set; }
public int EquipmentId { get; set; }
public string OvenName { get; set; } = string.Empty;
public string? SuggestedStartTime { get; set; }
public int EstimatedCycleMinutes { get; set; }
public decimal? CureTemperatureF { get; set; }
public decimal EstimatedSqFt { get; set; }
public decimal CapacityUtilization { get; set; }
public string? PrimaryColorName { get; set; }
public string? PrimaryColorCode { get; set; }
public string Rationale { get; set; } = string.Empty;
public List<SuggestedBatchItem> Items { get; set; } = new();
}
public class SuggestedBatchItem
{
public int JobId { get; set; }
public int JobItemId { get; set; }
public int JobItemCoatId { get; set; }
public string JobNumber { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string? ColorName { get; set; }
public string? ColorCode { get; set; }
public decimal SurfaceAreaSqFt { get; set; }
public int CoatPassNumber { get; set; }
public string CoatName { get; set; } = string.Empty;
public string Priority { get; set; } = "Normal";
}
// ──────────────────────────────────────────────────────────────────────────────
// View Models (used by the Web project)
// ──────────────────────────────────────────────────────────────────────────────
public class OvenSchedulerViewModel
{
public List<OvenInfoDto> Ovens { get; set; } = new();
public List<OvenBatchDto> Batches { get; set; } = new();
public List<QueuedJobDto> QueuedJobs { get; set; } = new();
public DateTime ScheduledDate { get; set; } = DateTime.Today;
public string OptimizationGoal { get; set; } = "maximize_throughput";
public int DefaultCycleMinutes { get; set; } = 45;
}
public class OvenInfoDto
{
public int OvenCostId { get; set; }
public int EquipmentId { get; set; }
public string Name { get; set; } = string.Empty;
public decimal? MaxLoadSqFt { get; set; }
public int CycleMinutes { get; set; }
public string Status { get; set; } = string.Empty;
public bool IsOperational { get; set; }
}
public class OvenBatchDto
{
public int Id { get; set; }
public string BatchNumber { get; set; } = string.Empty;
public int OvenCostId { get; set; }
public int EquipmentId { get; set; }
public string OvenName { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public int StatusId { get; set; }
public DateTime ScheduledDate { get; set; }
public DateTime? ScheduledStartTime { get; set; }
public DateTime? EstimatedEndTime { get; set; }
public DateTime? ActualStartTime { get; set; }
public DateTime? ActualEndTime { get; set; }
public decimal TotalSurfaceAreaSqFt { get; set; }
public decimal? MaxLoadSqFt { get; set; }
public decimal CapacityPct { get; set; }
public decimal? CureTemperatureF { get; set; }
public int CycleMinutes { get; set; }
public string? PrimaryColorName { get; set; }
public string? PrimaryColorCode { get; set; }
public bool AiSuggested { get; set; }
public string? AiReasoning { get; set; }
public string? Notes { get; set; }
public List<BatchItemDto> Items { get; set; } = new();
}
public class BatchItemDto
{
public int Id { get; set; }
public int JobId { get; set; }
public int JobItemId { get; set; }
public int JobItemCoatId { get; set; }
public string JobNumber { get; set; } = string.Empty;
public string CustomerName { get; set; } = string.Empty;
public string ItemDescription { get; set; } = string.Empty;
public string CoatName { get; set; } = string.Empty;
public string? ColorName { get; set; }
public string? ColorCode { get; set; }
public decimal SurfaceAreaContribution { get; set; }
public int CoatPassNumber { get; set; }
public string Priority { get; set; } = "Normal";
public DateTime? DueDate { get; set; }
public string ItemStatus { get; set; } = "Pending";
}
public class QueuedJobDto
{
public int JobId { get; set; }
public string JobNumber { get; set; } = string.Empty;
public string CustomerName { get; set; } = string.Empty;
public string Priority { get; set; } = "Normal";
public int PriorityId { get; set; }
public DateTime? DueDate { get; set; }
public bool IsOverdue { get; set; }
public string JobStatus { get; set; } = string.Empty;
public decimal TotalSqFt { get; set; }
public List<QueuedCoatDto> PendingCoats { get; set; } = new();
}
public class QueuedCoatDto
{
public int JobItemId { get; set; }
public int JobItemCoatId { get; set; }
public string ItemDescription { get; set; } = string.Empty;
public string CoatName { get; set; } = string.Empty;
public int CoatPassNumber { get; set; }
public string? ColorName { get; set; }
public string? ColorCode { get; set; }
public decimal SurfaceAreaSqFt { get; set; }
public decimal? CureTemperatureF { get; set; }
}
@@ -0,0 +1,27 @@
using System.ComponentModel.DataAnnotations;
using PowderCoating.Core.Enums;
namespace PowderCoating.Application.DTOs.ShopWorker;
public class CreateShopWorkerDto
{
[Required(ErrorMessage = "Worker name is required")]
[StringLength(100, ErrorMessage = "Name cannot exceed 100 characters")]
public string Name { get; set; } = string.Empty;
[Required(ErrorMessage = "Role is required")]
public ShopWorkerRole Role { get; set; } = ShopWorkerRole.GeneralLabor;
[Phone(ErrorMessage = "Invalid phone number format")]
[StringLength(20, ErrorMessage = "Phone cannot exceed 20 characters")]
public string? Phone { get; set; }
[EmailAddress(ErrorMessage = "Invalid email address format")]
[StringLength(100, ErrorMessage = "Email cannot exceed 100 characters")]
public string? Email { get; set; }
public bool IsActive { get; set; } = true;
[StringLength(500, ErrorMessage = "Notes cannot exceed 500 characters")]
public string? Notes { get; set; }
}
@@ -0,0 +1,16 @@
using PowderCoating.Core.Enums;
namespace PowderCoating.Application.DTOs.ShopWorker;
public class ShopWorkerDto
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public ShopWorkerRole Role { get; set; }
public string? Phone { get; set; }
public string? Email { get; set; }
public bool IsActive { get; set; }
public string? Notes { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
}
@@ -0,0 +1,29 @@
using System.ComponentModel.DataAnnotations;
using PowderCoating.Core.Enums;
namespace PowderCoating.Application.DTOs.ShopWorker;
public class UpdateShopWorkerDto
{
public int Id { get; set; }
[Required(ErrorMessage = "Worker name is required")]
[StringLength(100, ErrorMessage = "Name cannot exceed 100 characters")]
public string Name { get; set; } = string.Empty;
[Required(ErrorMessage = "Role is required")]
public ShopWorkerRole Role { get; set; }
[Phone(ErrorMessage = "Invalid phone number format")]
[StringLength(20, ErrorMessage = "Phone cannot exceed 20 characters")]
public string? Phone { get; set; }
[EmailAddress(ErrorMessage = "Invalid email address format")]
[StringLength(100, ErrorMessage = "Email cannot exceed 100 characters")]
public string? Email { get; set; }
public bool IsActive { get; set; }
[StringLength(500, ErrorMessage = "Notes cannot exceed 500 characters")]
public string? Notes { get; set; }
}
@@ -0,0 +1,24 @@
using PowderCoating.Core.Enums;
namespace PowderCoating.Application.DTOs.Subscription;
public record PlanLimitsDto(
int MaxUsers,
int MaxActiveJobs,
int MaxCustomers,
int MaxQuotes,
int MaxCatalogItems,
int CurrentUsers,
int CurrentJobs,
int CurrentCustomers,
int CurrentQuotes,
int CurrentCatalogItems,
int Plan);
public record SubscriptionStatusDto(
SubscriptionStatus Status,
int Plan,
DateTime? EndDate,
int? DaysRemaining,
bool IsGracePeriod,
bool IsExpired);
@@ -0,0 +1,75 @@
using System.ComponentModel.DataAnnotations;
namespace PowderCoating.Application.DTOs.Subscription;
public class SubscriptionPlanConfigDto
{
public int Id { get; set; }
public int Plan { get; set; }
public string DisplayName { get; set; } = string.Empty;
public string? Description { get; set; }
public int MaxUsers { get; set; }
public int MaxActiveJobs { get; set; }
public int MaxCustomers { get; set; }
public int MaxQuotes { get; set; }
public int MaxCatalogItems { get; set; }
public int MaxJobPhotos { get; set; }
public int MaxQuotePhotos { get; set; }
public int MaxAiPhotoQuotesPerMonth { get; set; }
public decimal MonthlyPrice { get; set; }
public decimal AnnualPrice { get; set; }
public string? StripePriceIdMonthly { get; set; }
public string? StripePriceIdAnnual { get; set; }
public bool AllowOnlinePayments { get; set; }
public bool AllowAccounting { get; set; }
public bool AllowAiPhotoQuotes { get; set; }
public bool AllowAiInventoryAssist { get; set; }
public bool IsActive { get; set; }
public int SortOrder { get; set; }
}
public class UpdateSubscriptionPlanConfigDto
{
public int Id { get; set; }
public string? Description { get; set; }
[Range(-1, int.MaxValue, ErrorMessage = "Value must be -1 (unlimited) or a positive number")]
public int MaxUsers { get; set; }
[Range(-1, int.MaxValue, ErrorMessage = "Value must be -1 (unlimited) or a positive number")]
public int MaxActiveJobs { get; set; }
[Range(-1, int.MaxValue, ErrorMessage = "Value must be -1 (unlimited) or a positive number")]
public int MaxCustomers { get; set; }
[Range(-1, int.MaxValue, ErrorMessage = "Value must be -1 (unlimited) or a positive number")]
public int MaxQuotes { get; set; }
[Range(-1, int.MaxValue, ErrorMessage = "Value must be -1 (unlimited) or a positive number")]
public int MaxCatalogItems { get; set; }
[Range(-1, int.MaxValue, ErrorMessage = "Value must be -1 (unlimited) or a positive number")]
public int MaxJobPhotos { get; set; }
[Range(-1, int.MaxValue, ErrorMessage = "Value must be -1 (unlimited) or a positive number")]
public int MaxQuotePhotos { get; set; }
[Range(-1, int.MaxValue, ErrorMessage = "Value must be -1 (unlimited) or a positive number")]
public int MaxAiPhotoQuotesPerMonth { get; set; }
[Range(0, 99999, ErrorMessage = "Price must be between 0 and 99999")]
public decimal MonthlyPrice { get; set; }
[Range(0, 99999, ErrorMessage = "Price must be between 0 and 99999")]
public decimal AnnualPrice { get; set; }
public string? StripePriceIdMonthly { get; set; }
public string? StripePriceIdAnnual { get; set; }
public bool AllowOnlinePayments { get; set; }
public bool AllowAccounting { get; set; }
public bool AllowAiPhotoQuotes { get; set; }
public bool AllowAiInventoryAssist { get; set; }
public bool IsActive { get; set; }
}
@@ -0,0 +1,85 @@
namespace PowderCoating.Application.DTOs.User;
/// <summary>
/// Platform user list item for SuperAdmin management
/// </summary>
public class PlatformUserListDto
{
public string Id { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public string FullName => $"{FirstName} {LastName}";
public string? EmployeeNumber { get; set; }
public string? Department { get; set; }
public string? Position { get; set; }
public bool IsSuperAdmin { get; set; }
public bool IsActive { get; set; }
public bool IsBanned { get; set; }
public int? CompanyId { get; set; }
public string? CompanyName { get; set; }
public string? CompanyRole { get; set; }
public DateTime? LastLoginDate { get; set; }
public DateTime CreatedAt { get; set; }
}
/// <summary>
/// DTO for creating a new SuperAdmin user
/// </summary>
public class CreateSuperAdminDto
{
public string Email { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public string? EmployeeNumber { get; set; }
public string? Department { get; set; } = "Platform";
public string? Position { get; set; } = "Super Administrator";
public string? Phone { get; set; }
public DateTime HireDate { get; set; } = DateTime.UtcNow;
public string Password { get; set; } = string.Empty;
public string ConfirmPassword { get; set; } = string.Empty;
}
/// <summary>
/// DTO for updating a SuperAdmin user
/// </summary>
public class UpdateSuperAdminDto
{
public string Id { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public string? EmployeeNumber { get; set; }
public string? Department { get; set; }
public string? Position { get; set; }
public string? Phone { get; set; }
public bool IsActive { get; set; }
public DateTime HireDate { get; set; }
public DateTime? TerminationDate { get; set; }
}
/// <summary>
/// DTO for viewing full SuperAdmin details
/// </summary>
public class SuperAdminDetailsDto
{
public string Id { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public string FullName => $"{FirstName} {LastName}";
public string? EmployeeNumber { get; set; }
public string? Department { get; set; }
public string? Position { get; set; }
public string? Phone { get; set; }
public bool IsActive { get; set; }
public bool IsBanned { get; set; }
public DateTime? BannedAt { get; set; }
public string? BanReason { get; set; }
public bool EmailConfirmed { get; set; }
public DateTime HireDate { get; set; }
public DateTime? TerminationDate { get; set; }
public DateTime? LastLoginDate { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
}
@@ -0,0 +1,261 @@
using System.ComponentModel.DataAnnotations;
namespace PowderCoating.Application.DTOs.User;
/// <summary>
/// Full user details for company admin management
/// </summary>
public class CompanyUserDto
{
public string Id { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public string FullName => $"{FirstName} {LastName}";
public string? EmployeeNumber { get; set; }
public string? CompanyRole { get; set; }
public string? Department { get; set; }
public string? Position { get; set; }
public string? Phone { get; set; }
public bool IsActive { get; set; }
public bool EmailConfirmed { get; set; }
public DateTime HireDate { get; set; }
public DateTime? TerminationDate { get; set; }
public DateTime? LastLoginDate { get; set; }
// Permissions
public bool CanManageJobs { get; set; }
public bool CanManageInventory { get; set; }
public bool CanManageCustomers { get; set; }
public bool CanCreateQuotes { get; set; }
public bool CanApproveQuotes { get; set; }
public bool CanManageCalendar { get; set; }
public bool CanViewCalendar { get; set; }
public bool CanManageProducts { get; set; }
public bool CanViewProducts { get; set; }
public bool CanManageEquipment { get; set; }
public bool CanManageVendors { get; set; }
public bool CanManageMaintenance { get; set; }
public bool CanManageInvoices { get; set; }
public bool CanViewReports { get; set; }
}
/// <summary>
/// User list item for grid/list views
/// </summary>
public class CompanyUserListDto
{
public string Id { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public string FullName => $"{FirstName} {LastName}";
public string? CompanyRole { get; set; }
public string? Department { get; set; }
public bool IsActive { get; set; }
public bool IsBanned { get; set; }
public DateTime HireDate { get; set; }
public DateTime? LastLoginDate { get; set; }
}
/// <summary>
/// DTO for creating a new company user
/// </summary>
public class CreateCompanyUserDto
{
[Required(ErrorMessage = "Email is required")]
[EmailAddress(ErrorMessage = "Please enter a valid email address")]
[StringLength(200, ErrorMessage = "Email cannot exceed 200 characters")]
[Display(Name = "Email")]
public string Email { get; set; } = string.Empty;
[Required(ErrorMessage = "First name is required")]
[StringLength(100, ErrorMessage = "First name cannot exceed 100 characters")]
[Display(Name = "First Name")]
public string FirstName { get; set; } = string.Empty;
[Required(ErrorMessage = "Last name is required")]
[StringLength(100, ErrorMessage = "Last name cannot exceed 100 characters")]
[Display(Name = "Last Name")]
public string LastName { get; set; } = string.Empty;
[StringLength(50, ErrorMessage = "Employee number cannot exceed 50 characters")]
[Display(Name = "Employee Number")]
public string? EmployeeNumber { get; set; }
[Required(ErrorMessage = "Company role is required")]
[StringLength(50, ErrorMessage = "Company role cannot exceed 50 characters")]
[Display(Name = "Company Role")]
public string CompanyRole { get; set; } = "Viewer";
[StringLength(100, ErrorMessage = "Department cannot exceed 100 characters")]
[Display(Name = "Department")]
public string? Department { get; set; }
[StringLength(100, ErrorMessage = "Position cannot exceed 100 characters")]
[Display(Name = "Position")]
public string? Position { get; set; }
[Phone(ErrorMessage = "Please enter a valid phone number")]
[StringLength(20, ErrorMessage = "Phone cannot exceed 20 characters")]
[Display(Name = "Phone")]
public string? Phone { get; set; }
[Required(ErrorMessage = "Hire date is required")]
[Display(Name = "Hire Date")]
public DateTime HireDate { get; set; } = DateTime.UtcNow;
[Required(ErrorMessage = "Password is required")]
[StringLength(100, MinimumLength = 8, ErrorMessage = "Password must be at least 8 characters")]
[Display(Name = "Password")]
public string Password { get; set; } = string.Empty;
// Permissions
[Display(Name = "Can Manage Jobs")]
public bool CanManageJobs { get; set; }
[Display(Name = "Can Manage Inventory")]
public bool CanManageInventory { get; set; }
[Display(Name = "Can Manage Customers")]
public bool CanManageCustomers { get; set; }
[Display(Name = "Can Create Quotes")]
public bool CanCreateQuotes { get; set; }
[Display(Name = "Can Approve Quotes")]
public bool CanApproveQuotes { get; set; }
[Display(Name = "Can Manage Calendar")]
public bool CanManageCalendar { get; set; }
[Display(Name = "Can View Calendar")]
public bool CanViewCalendar { get; set; }
[Display(Name = "Can Manage Products")]
public bool CanManageProducts { get; set; }
[Display(Name = "Can View Products")]
public bool CanViewProducts { get; set; }
[Display(Name = "Can Manage Equipment")]
public bool CanManageEquipment { get; set; }
[Display(Name = "Can Manage Vendors")]
public bool CanManageVendors { get; set; }
[Display(Name = "Can Manage Maintenance")]
public bool CanManageMaintenance { get; set; }
[Display(Name = "Can Manage Invoices")]
public bool CanManageInvoices { get; set; }
[Display(Name = "Can View Reports")]
public bool CanViewReports { get; set; }
[Display(Name = "Send Welcome Email")]
public bool SendWelcomeEmail { get; set; } = true;
}
/// <summary>
/// DTO for updating an existing company user
/// </summary>
public class UpdateCompanyUserDto
{
[Required]
public string Id { get; set; } = string.Empty;
[Required(ErrorMessage = "Email is required")]
[EmailAddress(ErrorMessage = "Please enter a valid email address")]
[StringLength(200, ErrorMessage = "Email cannot exceed 200 characters")]
[Display(Name = "Email")]
public string Email { get; set; } = string.Empty;
[Required(ErrorMessage = "First name is required")]
[StringLength(100, ErrorMessage = "First name cannot exceed 100 characters")]
[Display(Name = "First Name")]
public string FirstName { get; set; } = string.Empty;
[Required(ErrorMessage = "Last name is required")]
[StringLength(100, ErrorMessage = "Last name cannot exceed 100 characters")]
[Display(Name = "Last Name")]
public string LastName { get; set; } = string.Empty;
[StringLength(50, ErrorMessage = "Employee number cannot exceed 50 characters")]
[Display(Name = "Employee Number")]
public string? EmployeeNumber { get; set; }
[Required(ErrorMessage = "Company role is required")]
[StringLength(50, ErrorMessage = "Company role cannot exceed 50 characters")]
[Display(Name = "Company Role")]
public string CompanyRole { get; set; } = "Viewer";
[StringLength(100, ErrorMessage = "Department cannot exceed 100 characters")]
[Display(Name = "Department")]
public string? Department { get; set; }
[StringLength(100, ErrorMessage = "Position cannot exceed 100 characters")]
[Display(Name = "Position")]
public string? Position { get; set; }
[Phone(ErrorMessage = "Please enter a valid phone number")]
[StringLength(20, ErrorMessage = "Phone cannot exceed 20 characters")]
[Display(Name = "Phone")]
public string? Phone { get; set; }
[Display(Name = "Active")]
public bool IsActive { get; set; }
[Required(ErrorMessage = "Hire date is required")]
[Display(Name = "Hire Date")]
public DateTime HireDate { get; set; }
[Display(Name = "Termination Date")]
public DateTime? TerminationDate { get; set; }
// Permissions
[Display(Name = "Can Manage Jobs")]
public bool CanManageJobs { get; set; }
[Display(Name = "Can Manage Inventory")]
public bool CanManageInventory { get; set; }
[Display(Name = "Can Manage Customers")]
public bool CanManageCustomers { get; set; }
[Display(Name = "Can Create Quotes")]
public bool CanCreateQuotes { get; set; }
[Display(Name = "Can Approve Quotes")]
public bool CanApproveQuotes { get; set; }
[Display(Name = "Can Manage Calendar")]
public bool CanManageCalendar { get; set; }
[Display(Name = "Can View Calendar")]
public bool CanViewCalendar { get; set; }
[Display(Name = "Can Manage Products")]
public bool CanManageProducts { get; set; }
[Display(Name = "Can View Products")]
public bool CanViewProducts { get; set; }
[Display(Name = "Can Manage Equipment")]
public bool CanManageEquipment { get; set; }
[Display(Name = "Can Manage Vendors")]
public bool CanManageVendors { get; set; }
[Display(Name = "Can Manage Maintenance")]
public bool CanManageMaintenance { get; set; }
[Display(Name = "Can Manage Invoices")]
public bool CanManageInvoices { get; set; }
[Display(Name = "Can View Reports")]
public bool CanViewReports { get; set; }
}
@@ -0,0 +1,72 @@
using System.ComponentModel.DataAnnotations;
namespace PowderCoating.Application.DTOs.User;
public class UserProfileDto
{
public string Id { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public string FullName { get; set; } = string.Empty;
public string? Phone { get; set; }
public string? Department { get; set; }
public string? Position { get; set; }
public string? EmployeeNumber { get; set; }
public string? CompanyRole { get; set; }
public string Theme { get; set; } = "light";
public string SidebarColor { get; set; } = "ocean";
public string DateFormat { get; set; } = "MM/dd/yyyy";
public string? TimeZone { get; set; }
public bool HasProfilePicture { get; set; }
public DateTime HireDate { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? LastLoginDate { get; set; }
}
public class UpdateProfileDto
{
[Required]
[StringLength(100)]
public string FirstName { get; set; } = string.Empty;
[Required]
[StringLength(100)]
public string LastName { get; set; } = string.Empty;
[Phone]
public string? Phone { get; set; }
}
public class ChangePasswordDto
{
[Required]
public string CurrentPassword { get; set; } = string.Empty;
[Required]
[StringLength(100, MinimumLength = 8)]
public string NewPassword { get; set; } = string.Empty;
[Required]
[Compare("NewPassword", ErrorMessage = "Passwords do not match.")]
public string ConfirmPassword { get; set; } = string.Empty;
}
public class UpdateEmailDto
{
[Required]
[EmailAddress]
[StringLength(200)]
public string NewEmail { get; set; } = string.Empty;
[Required]
public string CurrentPassword { get; set; } = string.Empty;
}
public class UpdateAppearanceDto
{
public string Theme { get; set; } = "light";
public string SidebarColor { get; set; } = "ocean";
public string DateFormat { get; set; } = "MM/dd/yyyy";
public string? TimeZone { get; set; }
}
+206
View File
@@ -0,0 +1,206 @@
using System.ComponentModel.DataAnnotations;
namespace PowderCoating.Application.DTOs.Vendor;
// ============================================================================
// LIST DTO - For Index page listing
// ============================================================================
public class VendorListDto
{
public int Id { get; set; }
public string CompanyName { get; set; } = string.Empty;
public string? ContactName { get; set; }
public string? Phone { get; set; }
public string? Email { get; set; }
public bool IsActive { get; set; }
public bool IsPreferred { get; set; }
public int InventoryItemCount { get; set; }
}
// ============================================================================
// FULL VENDOR DTO - For Details page
// ============================================================================
public class VendorDto
{
public int Id { get; set; }
public string CompanyName { get; set; } = string.Empty;
public string? ContactName { get; set; }
public string? Phone { get; set; }
public string? Email { get; set; }
public string? Website { get; set; }
public string? Address { get; set; }
public string? City { get; set; }
public string? State { get; set; }
public string? ZipCode { get; set; }
public string? Country { get; set; }
public string? AccountNumber { get; set; }
public string? TaxId { get; set; }
public string? PaymentTerms { get; set; }
public decimal? CreditLimit { get; set; }
public string? Notes { get; set; }
public bool IsActive { get; set; }
public bool IsPreferred { get; set; }
public int? DefaultExpenseAccountId { get; set; }
public string? DefaultExpenseAccountName { get; set; }
public DateTime CreatedAt { get; set; }
}
// ============================================================================
// CREATE VENDOR DTO - For creating new vendors
// ============================================================================
public class CreateVendorDto
{
[Required]
[Display(Name = "Vendor Name")]
[StringLength(200)]
public string CompanyName { get; set; } = string.Empty;
[Display(Name = "Contact Name")]
[StringLength(100)]
public string? ContactName { get; set; }
[Display(Name = "Phone")]
[Phone]
[StringLength(20)]
public string? Phone { get; set; }
[Display(Name = "Email")]
[EmailAddress]
[StringLength(100)]
public string? Email { get; set; }
[Display(Name = "Website")]
[Url]
[StringLength(200)]
public string? Website { get; set; }
[Display(Name = "Address")]
[StringLength(200)]
public string? Address { get; set; }
[Display(Name = "City")]
[StringLength(100)]
public string? City { get; set; }
[Display(Name = "State")]
[StringLength(2)]
public string? State { get; set; }
[Display(Name = "Zip Code")]
[StringLength(10)]
public string? ZipCode { get; set; }
[Display(Name = "Country")]
[StringLength(50)]
public string? Country { get; set; }
[Display(Name = "Account Number")]
[StringLength(50)]
public string? AccountNumber { get; set; }
[Display(Name = "Tax ID / EIN")]
[StringLength(50)]
public string? TaxId { get; set; }
[Display(Name = "Payment Terms")]
[StringLength(100)]
public string? PaymentTerms { get; set; }
[Display(Name = "Credit Limit")]
[Range(0, 10000000)]
public decimal? CreditLimit { get; set; }
[Display(Name = "Notes")]
[DataType(DataType.MultilineText)]
public string? Notes { get; set; }
[Display(Name = "Active")]
public bool IsActive { get; set; } = true;
[Display(Name = "Preferred Vendor")]
public bool IsPreferred { get; set; } = false;
[Display(Name = "Default Expense Account")]
public int? DefaultExpenseAccountId { get; set; }
}
// ============================================================================
// UPDATE VENDOR DTO - For editing existing vendors
// ============================================================================
public class UpdateVendorDto
{
public int Id { get; set; }
[Required]
[Display(Name = "Vendor Name")]
[StringLength(200)]
public string CompanyName { get; set; } = string.Empty;
[Display(Name = "Contact Name")]
[StringLength(100)]
public string? ContactName { get; set; }
[Display(Name = "Phone")]
[Phone]
[StringLength(20)]
public string? Phone { get; set; }
[Display(Name = "Email")]
[EmailAddress]
[StringLength(100)]
public string? Email { get; set; }
[Display(Name = "Website")]
[Url]
[StringLength(200)]
public string? Website { get; set; }
[Display(Name = "Address")]
[StringLength(200)]
public string? Address { get; set; }
[Display(Name = "City")]
[StringLength(100)]
public string? City { get; set; }
[Display(Name = "State")]
[StringLength(2)]
public string? State { get; set; }
[Display(Name = "Zip Code")]
[StringLength(10)]
public string? ZipCode { get; set; }
[Display(Name = "Country")]
[StringLength(50)]
public string? Country { get; set; }
[Display(Name = "Account Number")]
[StringLength(50)]
public string? AccountNumber { get; set; }
[Display(Name = "Tax ID / EIN")]
[StringLength(50)]
public string? TaxId { get; set; }
[Display(Name = "Payment Terms")]
[StringLength(100)]
public string? PaymentTerms { get; set; }
[Display(Name = "Credit Limit")]
[Range(0, 10000000)]
public decimal? CreditLimit { get; set; }
[Display(Name = "Notes")]
[DataType(DataType.MultilineText)]
public string? Notes { get; set; }
[Display(Name = "Active")]
public bool IsActive { get; set; }
[Display(Name = "Preferred Vendor")]
public bool IsPreferred { get; set; }
[Display(Name = "Default Expense Account")]
public int? DefaultExpenseAccountId { get; set; }
}
@@ -0,0 +1,354 @@
using System.ComponentModel.DataAnnotations;
using PowderCoating.Core.Enums;
namespace PowderCoating.Application.DTOs.Wizard;
/// <summary>
/// Tracks the current user's wizard progress (read from CompanyPreferences)
/// </summary>
public class WizardProgressDto
{
public bool Started { get; set; }
public bool Completed { get; set; }
public List<int> DoneSteps { get; set; } = new();
public List<int> SkippedSteps { get; set; } = new();
public const int TotalSteps = 10;
public bool IsStepDone(int step) => DoneSteps.Contains(step);
public bool IsStepSkipped(int step) => SkippedSteps.Contains(step);
public bool IsStepTouched(int step) => IsStepDone(step) || IsStepSkipped(step);
public int CompletedCount => DoneSteps.Count + SkippedSteps.Count;
public int ProgressPercent => TotalSteps == 0 ? 0 : (int)Math.Round((double)CompletedCount / TotalSteps * 100);
}
// ─── Step 1: Company Profile ────────────────────────────────────────────────
public class WizardStep1Dto
{
[Required, MaxLength(200)]
[Display(Name = "Company Name")]
public string CompanyName { get; set; } = string.Empty;
[MaxLength(200)]
[Display(Name = "Primary Contact Name")]
public string? PrimaryContactName { get; set; }
[EmailAddress, MaxLength(200)]
[Display(Name = "Primary Contact Email")]
public string? PrimaryContactEmail { get; set; }
[Phone, MaxLength(50)]
[Display(Name = "Phone")]
public string? Phone { get; set; }
[MaxLength(300)]
[Display(Name = "Address")]
public string? Address { get; set; }
[MaxLength(100)]
[Display(Name = "City")]
public string? City { get; set; }
[MaxLength(100)]
[Display(Name = "State")]
public string? State { get; set; }
[MaxLength(20)]
[Display(Name = "ZIP Code")]
public string? ZipCode { get; set; }
[MaxLength(100)]
[Display(Name = "Time Zone")]
public string? TimeZone { get; set; }
[MaxLength(10)]
[Display(Name = "Default Currency")]
public string DefaultCurrency { get; set; } = "USD";
[Display(Name = "Use Metric System (metres / kilograms)")]
public bool UseMetricSystem { get; set; } = false;
}
// ─── Step 2: QuickBooks Migration ───────────────────────────────────────────
public class WizardStep2QbDto
{
public bool MigratingFromQuickBooks { get; set; } = false;
}
// ─── Step 3: Operating Costs (was Step 2) ───────────────────────────────────
public class WizardStep2Dto
{
[Range(0, 10000)]
[Display(Name = "Standard Labor Rate ($/hr)")]
public decimal StandardLaborRate { get; set; }
[Range(0, 10000)]
[Display(Name = "Sandblaster Cost ($/hr)")]
public decimal SandblasterCostPerHour { get; set; }
[Range(0, 10000)]
[Display(Name = "Coating Booth Cost ($/hr)")]
public decimal CoatingBoothCostPerHour { get; set; }
[Range(0, 10000)]
[Display(Name = "Oven Operating Cost ($/hr)")]
public decimal OvenOperatingCostPerHour { get; set; }
[Range(0, 1000)]
[Display(Name = "Powder Coating Cost ($/sq ft)")]
public decimal PowderCoatingCostPerSqFt { get; set; }
[Range(0, 100)]
[Display(Name = "General Markup (%)")]
public decimal GeneralMarkupPercentage { get; set; }
[Range(0, 100)]
[Display(Name = "Tax Rate (%)")]
public decimal TaxPercent { get; set; }
[Range(0, 100000)]
[Display(Name = "Shop Minimum Charge ($)")]
public decimal ShopMinimumCharge { get; set; }
[Display(Name = "Shop Capability Tier")]
public ShopCapabilityTier ShopCapabilityTier { get; set; } = ShopCapabilityTier.Small;
}
// ─── Step 3: Branding & Numbering ───────────────────────────────────────────
public class WizardStep3Dto
{
[MaxLength(10)]
[Display(Name = "Quote Number Prefix")]
public string QuoteNumberPrefix { get; set; } = "QT";
[MaxLength(10)]
[Display(Name = "Job Number Prefix")]
public string JobNumberPrefix { get; set; } = "JOB";
[MaxLength(10)]
[Display(Name = "Invoice Number Prefix")]
public string InvoiceNumberPrefix { get; set; } = "INV";
[MaxLength(7)]
[Display(Name = "Quote Accent Color")]
public string QtAccentColor { get; set; } = "#374151";
[MaxLength(7)]
[Display(Name = "Invoice Accent Color")]
public string InAccentColor { get; set; } = "#374151";
[MaxLength(7)]
[Display(Name = "Work Order Accent Color")]
public string WoAccentColor { get; set; } = "#374151";
}
// ─── Step 4: Invoice & Quote Defaults ───────────────────────────────────────
public class WizardStep4Dto
{
[MaxLength(100)]
[Display(Name = "Default Payment Terms")]
public string DefaultPaymentTerms { get; set; } = "Net 30";
[Range(1, 365)]
[Display(Name = "Quote Validity (days)")]
public int DefaultQuoteValidityDays { get; set; } = 30;
[Range(1, 365)]
[Display(Name = "Default Turnaround (days)")]
public int DefaultTurnaroundDays { get; set; } = 7;
[MaxLength(2000)]
[Display(Name = "Default Quote Terms & Conditions")]
public string? QtDefaultTerms { get; set; }
[MaxLength(500)]
[Display(Name = "Quote Footer Note")]
public string? QtFooterNote { get; set; }
}
// ─── Step 5: Job & Workflow ──────────────────────────────────────────────────
public class WizardStep5Dto
{
[Display(Name = "Default Job Priority")]
public string DefaultJobPriority { get; set; } = "Normal";
[Display(Name = "Require Customer PO Number")]
public bool RequireCustomerPO { get; set; } = false;
[Display(Name = "Allow Customer Approval via Link")]
public bool AllowCustomerApproval { get; set; } = true;
}
// ─── Step 6: Chart of Accounts (review only) ────────────────────────────────
public class WizardStep6Dto
{
public int AccountCount { get; set; }
public int RevenueAccounts { get; set; }
public int ExpenseAccounts { get; set; }
public int AssetAccounts { get; set; }
}
// ─── Step 7: Notification Preferences ───────────────────────────────────────
public class WizardStep7Dto
{
[Display(Name = "Enable Email Notifications")]
public bool EmailNotificationsEnabled { get; set; } = true;
[EmailAddress, MaxLength(200)]
[Display(Name = "Send Emails From Address")]
public string? EmailFromAddress { get; set; }
[MaxLength(200)]
[Display(Name = "From Display Name")]
public string? EmailFromName { get; set; }
[Display(Name = "Notify on New Job Created")]
public bool NotifyOnNewJob { get; set; } = true;
[Display(Name = "Notify on New Quote Created")]
public bool NotifyOnNewQuote { get; set; } = true;
[Display(Name = "Notify on Job Status Change")]
public bool NotifyOnJobStatusChange { get; set; } = true;
[Display(Name = "Notify on Quote Approval")]
public bool NotifyOnQuoteApproval { get; set; } = true;
[Display(Name = "Notify on Payment Received")]
public bool NotifyOnPaymentReceived { get; set; } = true;
[Display(Name = "Enable Automated Payment Reminders")]
public bool PaymentRemindersEnabled { get; set; } = false;
[MaxLength(50)]
[Display(Name = "Reminder Days (comma-separated, days past due)")]
public string PaymentReminderDays { get; set; } = "7,14,30";
// Alert Thresholds
[Range(0, 90)]
[Display(Name = "Quote Expiry Warning (days before expiry)")]
public int QuoteExpiryWarningDays { get; set; } = 3;
[Range(0, 90)]
[Display(Name = "Due Date Warning (days before due)")]
public int DueDateWarningDays { get; set; } = 2;
[Range(0, 90)]
[Display(Name = "Maintenance Alert (days before scheduled date)")]
public int MaintenanceAlertDays { get; set; } = 7;
}
// ─── Step 8: Inventory / Powder Colors ──────────────────────────────────────
public class WizardStep8Dto
{
/// <summary>JSON array of WizardInventoryItemDto submitted as a hidden field</summary>
public string? ItemsJson { get; set; }
}
public class WizardInventoryItemDto
{
public string Name { get; set; } = string.Empty;
public string? ColorName { get; set; }
public string? ColorCode { get; set; }
public string? Manufacturer { get; set; }
public string? Finish { get; set; }
public decimal UnitCost { get; set; }
public decimal QuantityOnHand { get; set; }
}
// ─── Step 9: Team Members ────────────────────────────────────────────────────
public class WizardStep9Dto
{
/// <summary>JSON array of WizardTeamMemberDto submitted as a hidden field</summary>
public string? MembersJson { get; set; }
}
public class WizardTeamMemberDto
{
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public string CompanyRole { get; set; } = "Worker";
}
// ─── Step 10: Vendors & Suppliers ────────────────────────────────────────────
public class WizardStep10Dto
{
/// <summary>JSON array of WizardVendorDto submitted as a hidden field</summary>
public string? VendorsJson { get; set; }
}
public class WizardVendorDto
{
public string CompanyName { get; set; } = string.Empty;
public string? ContactName { get; set; }
public string? Email { get; set; }
public string? Phone { get; set; }
public string? Website { get; set; }
}
// ─── Step 14: Equipment / Named Ovens + Blast Setups ────────────────────────
public class WizardOvensStepDto
{
/// <summary>JSON array of WizardOvenDto submitted as a hidden field</summary>
public string? OvensJson { get; set; }
/// <summary>JSON array of WizardBlastSetupDto submitted as a hidden field</summary>
public string? BlastSetupsJson { get; set; }
}
public class WizardOvenDto
{
public int Id { get; set; }
public string Label { get; set; } = string.Empty;
public decimal CostPerHour { get; set; }
public decimal? MaxLoadSqFt { get; set; }
public int? DefaultCycleMinutes { get; set; }
}
public class WizardBlastSetupDto
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
/// <summary>Maps to BlastSetupType enum: 0=SiphonCabinet, 1=SiphonPot, 2=PressurePot, 3=WetBlasting</summary>
public int SetupType { get; set; } = 2;
public decimal CompressorCfm { get; set; }
/// <summary>Nozzle orifice number 28.</summary>
public int BlastNozzleSize { get; set; } = 5;
/// <summary>Maps to BlastSubstrateType enum: 0=Paint, 1=PowderCoat, 2=RustAndScale, 3=Mixed</summary>
public int PrimarySubstrate { get; set; } = 3;
public decimal? BlastRateSqFtPerHourOverride { get; set; }
public bool IsDefault { get; set; }
}
// ─── Step 15: Pricing Tiers ──────────────────────────────────────────────────
public class WizardPricingTiersStepDto
{
/// <summary>JSON array of WizardPricingTierDto submitted as a hidden field</summary>
public string? TiersJson { get; set; }
}
public class WizardPricingTierDto
{
public int Id { get; set; }
public string TierName { get; set; } = string.Empty;
public string? Description { get; set; }
public decimal DiscountPercent { get; set; }
}
// ─── Step 16: Service Catalog ────────────────────────────────────────────────
public class WizardCatalogStepDto
{
/// <summary>JSON array of WizardCatalogItemDto submitted as a hidden field</summary>
public string? ItemsJson { get; set; }
}
public class WizardCatalogItemDto
{
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public decimal DefaultPrice { get; set; }
public decimal? ApproximateArea { get; set; }
}
@@ -0,0 +1,33 @@
namespace PowderCoating.Application.Interfaces;
/// <summary>
/// Keeps Account.CurrentBalance in sync with double-entry transactions.
/// DebitAsync / CreditAsync update the balance in the normal-balance direction
/// for each account sub-type, but do NOT call CompleteAsync — the caller must
/// persist by calling IUnitOfWork.CompleteAsync / CommitTransactionAsync.
/// RecalculateAllAsync is the exception: it saves internally and is safe to call standalone.
/// </summary>
public interface IAccountBalanceService
{
/// <summary>
/// Applies a debit to the account.
/// Debit-normal accounts (Asset / Expense / COGS): balance increases.
/// Credit-normal accounts (Liability / Equity / Revenue): balance decreases.
/// No-op when accountId is null or amount is zero.
/// </summary>
Task DebitAsync(int? accountId, decimal amount);
/// <summary>
/// Applies a credit to the account.
/// Debit-normal accounts: balance decreases.
/// Credit-normal accounts: balance increases.
/// No-op when accountId is null or amount is zero.
/// </summary>
Task CreditAsync(int? accountId, decimal amount);
/// <summary>
/// Recomputes CurrentBalance for every active account in the company by replaying all
/// transactions through LedgerService. Saves internally. Use after import or to fix drift.
/// </summary>
Task RecalculateAllAsync(int companyId);
}
@@ -0,0 +1,46 @@
using PowderCoating.Application.DTOs.AI;
namespace PowderCoating.Application.Interfaces;
public interface IAccountingAiService
{
/// <summary>
/// Scans a receipt/invoice image and extracts vendor, date, total, invoice number, and line items.
/// Attempts to match each line item to one of the provided expense accounts.
/// </summary>
Task<ReceiptScanResult> ScanReceiptAsync(
byte[] imageData,
string mimeType,
List<AccountSummary> availableAccounts);
/// <summary>
/// Drafts a follow-up email for an overdue AR customer.
/// Tone scales with days overdue: gentle (≤30), firm (31-60), serious (61+).
/// </summary>
Task<ArFollowUpResult> DraftFollowUpEmailAsync(ArFollowUpRequest request);
/// <summary>
/// Suggests the best-matching expense account for a bill line item or expense.
/// Returns a primary suggestion plus up to 3 ranked alternatives.
/// </summary>
Task<AccountSuggestionResult> SuggestAccountAsync(AccountSuggestionRequest request);
/// <summary>
/// Generates a plain-English financial health summary with 4-6 bullet points
/// and a sentiment classification (positive / neutral / concerning).
/// </summary>
Task<FinancialSummaryResult> GenerateFinancialSummaryAsync(FinancialSummaryRequest request);
/// <summary>
/// Projects 30/60/90-day cash position based on open AR, open AP, and active job pipeline.
/// Returns period-by-period inflow/outflow estimates and plain-English insights.
/// </summary>
Task<CashFlowForecastResult> GenerateCashFlowForecastAsync(CashFlowForecastRequest request);
/// <summary>
/// Scans recent bills and expense account trends for duplicates, unusual amounts,
/// and accounts running significantly above historical averages.
/// Returns a ranked list of flagged items with recommended actions.
/// </summary>
Task<AnomalyDetectionResult> DetectAnomaliesAsync(AnomalyDetectionRequest request);
}
@@ -0,0 +1,10 @@
namespace PowderCoating.Application.Interfaces;
public interface IAdminNotificationService
{
Task NotifyNewCompanyRegisteredAsync(int companyId, string companyName, string planName, string contactName, string contactEmail);
Task NotifyBugReportSubmittedAsync(int bugReportId, string title, string description, string priority, string submittedByName, string companyName);
Task NotifyCompanyExpiredAsync(int companyId, string companyName, string contactEmail, DateTime expiredOn);
Task NotifyCompanyGracePeriodAsync(int companyId, string companyName, string contactEmail, DateTime gracePeriodEndsOn);
Task NotifyContactFormSubmittedAsync(string senderName, string senderEmail, string companyName, string category, string subject, string message);
}
@@ -0,0 +1,23 @@
namespace PowderCoating.Application.Interfaces;
public interface IAiHelpService
{
/// <summary>
/// Send a message to the AI help assistant and get a response.
/// </summary>
/// <param name="conversationHistory">Prior turns: alternating user/assistant messages.</param>
/// <param name="userMessage">The current user message.</param>
/// <param name="tenantContext">Read-only context about the current user and company.</param>
Task<string> SendMessageAsync(
List<AiHelpMessage> conversationHistory,
string userMessage,
string systemPrompt);
}
public record AiHelpMessage(string Role, string Content);
public record AiHelpTenantContext(
string CompanyName,
string UserRole,
string UserName,
string? SubscriptionPlan);
@@ -0,0 +1,18 @@
using PowderCoating.Application.DTOs.AI;
using PowderCoating.Core.Entities;
namespace PowderCoating.Application.Interfaces;
public interface IAiQuoteService
{
/// <summary>
/// Analyze item photo(s) and return an estimated quote or a follow-up question.
/// </summary>
Task<AiAnalyzeItemResult> AnalyzeItemAsync(
AiAnalyzeItemRequest request,
List<(byte[] Data, string ContentType, string FileName)> photos,
CompanyOperatingCosts costs,
decimal avgPowderCostPerLb,
CompanyAiContext? context = null,
CompanyBlastSetup? selectedBlastSetup = null);
}
@@ -0,0 +1,8 @@
using PowderCoating.Application.DTOs.Scheduling;
namespace PowderCoating.Application.Interfaces;
public interface IAiSchedulingService
{
Task<BatchScheduleSuggestion> SuggestBatchesAsync(BatchSchedulingRequest request);
}
@@ -0,0 +1,18 @@
namespace PowderCoating.Application.Interfaces;
/// <summary>
/// Logs Anthropic API calls to the AiUsageLog table for platform-wide cost visibility.
/// Implementations must be fault-tolerant — a logging failure must never break the caller.
/// </summary>
public interface IAiUsageLogger
{
/// <summary>
/// Records a single AI API call. Fire-and-forget safe — swallows all exceptions internally.
/// </summary>
/// <param name="companyId">Tenant that triggered the call.</param>
/// <param name="userId">Identity user ID of the authenticated user.</param>
/// <param name="feature">One of the AppConstants.AiFeatures constants.</param>
/// <param name="success">False when the AI service threw or returned an error.</param>
/// <param name="inputLength">Character or byte count of the input — rough token-cost proxy.</param>
Task LogAsync(int companyId, string userId, string feature, bool success = true, int inputLength = 0);
}
@@ -0,0 +1,21 @@
namespace PowderCoating.Application.Interfaces;
/// <summary>
/// Writes manual entries to the AuditLog table for operations that are not captured
/// automatically by the EF SaveChanges interceptor — imports, exports, and future
/// platform-level events. Uses the same table and viewer as the interceptor so all
/// audit history is in one place.
/// </summary>
public interface IAuditService
{
/// <summary>
/// Appends a single row to AuditLogs outside of the EF change tracker.
/// </summary>
/// <param name="action">Verb describing the event, e.g. "Imported", "Exported".</param>
/// <param name="entityType">Logical category, e.g. "Customers", "AccountingExport".</param>
/// <param name="description">Short human-readable label shown in the audit viewer.</param>
/// <param name="details">Optional object serialised to JSON and stored in NewValues.</param>
/// <param name="entityId">Optional identifier for the affected record.</param>
Task LogAsync(string action, string entityType, string? description = null,
object? details = null, string? entityId = null);
}
@@ -0,0 +1,22 @@
namespace PowderCoating.Application.Interfaces;
public interface IAzureBlobStorageService
{
Task<(bool Success, string ErrorMessage)> UploadAsync(
string containerName,
string blobName,
Stream content,
string contentType);
Task<(bool Success, byte[] Content, string ContentType, string ErrorMessage)> DownloadAsync(
string containerName,
string blobName);
Task<(bool Success, string ErrorMessage)> DeleteAsync(
string containerName,
string blobName);
Task<bool> ExistsAsync(string containerName, string blobName);
Task<IEnumerable<string>> ListBlobsByPrefixAsync(string containerName, string prefix);
}
@@ -0,0 +1,20 @@
using PowderCoating.Application.DTOs.Health;
namespace PowderCoating.Application.Interfaces;
/// <summary>
/// Checks whether a tenant company's operational configuration is complete.
/// Separate from churn-risk health (login/engagement signals) — this catches
/// setup gaps that break features before the customer files a support ticket.
/// </summary>
public interface ICompanyConfigHealthService
{
/// <summary>Checks one company and returns its config issues.</summary>
Task<CompanyConfigHealth> CheckAsync(int companyId);
/// <summary>
/// Batch-checks multiple companies in a fixed number of DB round-trips
/// (one query per check type, not one query per company).
/// </summary>
Task<Dictionary<int, CompanyConfigHealth>> CheckBatchAsync(IEnumerable<int> companyIds);
}
@@ -0,0 +1,31 @@
using Microsoft.AspNetCore.Http;
namespace PowderCoating.Application.Interfaces;
public interface ICompanyLogoService
{
/// <summary>
/// Save company logo to filesystem
/// </summary>
Task<(bool Success, string FilePath, string ErrorMessage)> SaveCompanyLogoAsync(IFormFile file, int companyId);
/// <summary>
/// Delete company logo from filesystem
/// </summary>
Task<(bool Success, string ErrorMessage)> DeleteCompanyLogoAsync(string filePath);
/// <summary>
/// Get company logo from filesystem
/// </summary>
Task<(bool Success, byte[] FileContent, string ContentType, string ErrorMessage)> GetCompanyLogoAsync(string filePath);
/// <summary>
/// Check if company logo exists
/// </summary>
Task<bool> CompanyLogoExistsAsync(string filePath);
/// <summary>
/// Get the expected logo path for a company
/// </summary>
string GetCompanyLogoPath(int companyId, string extension);
}
@@ -0,0 +1,214 @@
using PowderCoating.Application.DTOs.Import;
namespace PowderCoating.Application.Interfaces;
/// <summary>
/// Service for bulk importing data via CSV files.
/// Supports customers, catalog items, and inventory items.
/// </summary>
public interface ICsvImportService
{
/// <summary>
/// Generate a CSV template file for customer imports.
/// </summary>
/// <returns>CSV file content as byte array</returns>
byte[] GenerateCustomerTemplate();
/// <summary>
/// Generate a CSV template file for catalog item imports.
/// </summary>
/// <returns>CSV file content as byte array</returns>
byte[] GenerateCatalogItemTemplate();
/// <summary>
/// Generate a CSV template file for inventory item imports.
/// </summary>
/// <returns>CSV file content as byte array</returns>
byte[] GenerateInventoryItemTemplate();
/// <summary>
/// Import customers from a CSV stream.
/// </summary>
/// <param name="csvStream">CSV file stream</param>
/// <param name="companyId">Company ID for multi-tenancy</param>
/// <returns>Import result with success/error counts</returns>
Task<CsvImportResultDto> ImportCustomersAsync(Stream csvStream, int companyId);
/// <summary>
/// Import catalog items from a CSV stream.
/// Creates categories on-the-fly if they don't exist.
/// </summary>
/// <param name="csvStream">CSV file stream</param>
/// <param name="companyId">Company ID for multi-tenancy</param>
/// <param name="revenueAccountId">Optional revenue account to assign to all imported items</param>
/// <param name="cogsAccountId">Optional COGS account to assign to all imported items</param>
/// <returns>Import result with success/error counts</returns>
Task<CsvImportResultDto> ImportCatalogItemsAsync(Stream csvStream, int companyId, int? revenueAccountId = null, int? cogsAccountId = null);
/// <summary>
/// Import inventory items from a CSV stream.
/// </summary>
/// <param name="csvStream">CSV file stream</param>
/// <param name="companyId">Company ID for multi-tenancy</param>
/// <param name="inventoryAccountId">Optional inventory asset account to assign to all imported items</param>
/// <param name="cogsAccountId">Optional COGS account to assign to all imported items</param>
/// <returns>Import result with success/error counts</returns>
Task<CsvImportResultDto> ImportInventoryItemsAsync(Stream csvStream, int companyId, int? inventoryAccountId = null, int? cogsAccountId = null);
/// <summary>
/// Generate a CSV template file for quote imports.
/// </summary>
/// <returns>CSV file content as byte array</returns>
byte[] GenerateQuoteTemplate();
/// <summary>
/// Import quotes from a CSV stream.
/// </summary>
/// <param name="csvStream">CSV file stream</param>
/// <param name="companyId">Company ID for multi-tenancy</param>
/// <returns>Import result with success/error counts</returns>
Task<CsvImportResultDto> ImportQuotesAsync(Stream csvStream, int companyId);
/// <summary>
/// Generate a CSV template file for job imports.
/// </summary>
/// <returns>CSV file content as byte array</returns>
byte[] GenerateJobTemplate();
/// <summary>
/// Import jobs from a CSV stream.
/// </summary>
/// <param name="csvStream">CSV file stream</param>
/// <param name="companyId">Company ID for multi-tenancy</param>
/// <returns>Import result with success/error counts</returns>
Task<CsvImportResultDto> ImportJobsAsync(Stream csvStream, int companyId);
/// <summary>
/// Generate a CSV template file for appointment imports.
/// </summary>
/// <returns>CSV file content as byte array</returns>
byte[] GenerateAppointmentTemplate();
/// <summary>
/// Import appointments from a CSV stream.
/// </summary>
/// <param name="csvStream">CSV file stream</param>
/// <param name="companyId">Company ID for multi-tenancy</param>
/// <returns>Import result with success/error counts</returns>
Task<CsvImportResultDto> ImportAppointmentsAsync(Stream csvStream, int companyId);
/// <summary>
/// Generate a CSV template file for equipment imports.
/// </summary>
/// <returns>CSV file content as byte array</returns>
byte[] GenerateEquipmentTemplate();
/// <summary>
/// Import equipment from a CSV stream.
/// </summary>
/// <param name="csvStream">CSV file stream</param>
/// <param name="companyId">Company ID for multi-tenancy</param>
/// <returns>Import result with success/error counts</returns>
Task<CsvImportResultDto> ImportEquipmentAsync(Stream csvStream, int companyId);
/// <summary>
/// Generate a CSV template file for maintenance record imports.
/// </summary>
/// <returns>CSV file content as byte array</returns>
byte[] GenerateMaintenanceTemplate();
/// <summary>
/// Import maintenance records from a CSV stream.
/// </summary>
/// <param name="csvStream">CSV file stream</param>
/// <param name="companyId">Company ID for multi-tenancy</param>
/// <returns>Import result with success/error counts</returns>
Task<CsvImportResultDto> ImportMaintenanceAsync(Stream csvStream, int companyId);
/// <summary>
/// Generate a CSV template file for vendor imports.
/// </summary>
byte[] GenerateVendorTemplate();
/// <summary>
/// Import vendors from a CSV stream.
/// Updates existing vendors matched by CompanyName; creates new ones otherwise.
/// </summary>
Task<CsvImportResultDto> ImportVendorsAsync(Stream csvStream, int companyId);
/// <summary>
/// Generate a CSV template file for shop worker imports.
/// </summary>
byte[] GenerateShopWorkerTemplate();
/// <summary>
/// Import shop workers from a CSV stream.
/// Updates existing workers matched by Name; creates new ones otherwise.
/// </summary>
Task<CsvImportResultDto> ImportShopWorkersAsync(Stream csvStream, int companyId);
/// <summary>
/// Generate a CSV template file for prep service imports.
/// </summary>
byte[] GeneratePrepServiceTemplate();
/// <summary>
/// Import prep services from a CSV stream.
/// Updates existing services matched by ServiceName; creates new ones otherwise.
/// </summary>
Task<CsvImportResultDto> ImportPrepServicesAsync(Stream csvStream, int companyId);
/// <summary>
/// Generate a CSV template file for expense imports.
/// </summary>
byte[] GenerateExpenseTemplate();
/// <summary>
/// Import expenses from a CSV stream.
/// ExpenseAccountNumber and PaymentAccountNumber are resolved by Account.AccountNumber.
/// VendorName and JobNumber are optional lookups. ExpenseNumber is auto-generated when blank.
/// </summary>
Task<CsvImportResultDto> ImportExpensesAsync(Stream csvStream, int companyId);
/// <summary>
/// Generate a CSV template file for Chart of Accounts imports.
/// </summary>
byte[] GenerateChartOfAccountsTemplate();
/// <summary>
/// Import Chart of Accounts entries from a CSV stream.
/// Existing accounts matched by AccountNumber are updated; new ones are created.
/// System accounts (IsSystem=true) are never modified by import.
/// </summary>
Task<CsvImportResultDto> ImportChartOfAccountsAsync(Stream csvStream, int companyId);
/// <summary>
/// Generate a CSV template file for invoice imports with headers matching the native export.
/// </summary>
byte[] GenerateInvoiceTemplate();
/// <summary>
/// Import invoice headers from a CSV stream. Customers are resolved by CustomerEmail then
/// CustomerName. Duplicate detection uses InvoiceNumber as the unique key. Existing invoices
/// are updated; new ones are created. Line items are not part of the CSV format.
/// </summary>
Task<CsvImportResultDto> ImportInvoicesAsync(Stream csvStream, int companyId);
/// <summary>Generate a CSV template file for payment imports.</summary>
byte[] GeneratePaymentTemplate();
/// <summary>
/// Import payment records from a CSV stream. Invoices are resolved by InvoiceNumber.
/// Duplicates are detected by InvoiceNumber + PaymentDate + Amount and skipped.
/// </summary>
Task<CsvImportResultDto> ImportPaymentsAsync(Stream csvStream, int companyId);
/// <summary>Generate a CSV template file for purchase order imports.</summary>
byte[] GeneratePurchaseOrderTemplate();
/// <summary>
/// Import purchase order headers from a CSV stream. Vendors are resolved by company name.
/// Existing POs matched by PoNumber are updated; new ones are created.
/// </summary>
Task<CsvImportResultDto> ImportPurchaseOrdersAsync(Stream csvStream, int companyId);
}
@@ -0,0 +1,40 @@
namespace PowderCoating.Application.Interfaces;
/// <summary>
/// Represents a file to attach to an outbound email.
/// </summary>
public record EmailAttachment(byte[] Data, string Filename, string ContentType);
public interface IEmailService
{
/// <summary>
/// Sends a plain or HTML email, optionally with a single file attachment (used for invoice
/// and quote PDFs). For multiple attachments such as job photos use
/// <see cref="SendEmailWithAttachmentsAsync"/>.
/// </summary>
Task<(bool Success, string? ErrorMessage)> SendEmailAsync(
string toEmail,
string toName,
string subject,
string plainTextBody,
string? htmlBody = null,
byte[]? attachmentData = null,
string? attachmentFilename = null,
string? attachmentContentType = null,
string? replyToEmail = null,
string? replyToName = null);
/// <summary>
/// Sends an email with one or more file attachments (e.g. job photos). Callers are
/// responsible for keeping total attachment size under SendGrid's 30 MB per-message limit.
/// </summary>
Task<(bool Success, string? ErrorMessage)> SendEmailWithAttachmentsAsync(
string toEmail,
string toName,
string subject,
string plainTextBody,
string? htmlBody = null,
IList<EmailAttachment>? attachments = null,
string? replyToEmail = null,
string? replyToName = null);
}
@@ -0,0 +1,26 @@
using Microsoft.AspNetCore.Http;
namespace PowderCoating.Application.Interfaces;
public interface IEquipmentManualService
{
/// <summary>
/// Save equipment manual to filesystem
/// </summary>
Task<(bool Success, string FilePath, string ErrorMessage)> SaveEquipmentManualAsync(IFormFile file, int companyId, int equipmentId);
/// <summary>
/// Delete equipment manual from filesystem
/// </summary>
Task<(bool Success, string ErrorMessage)> DeleteEquipmentManualAsync(string filePath);
/// <summary>
/// Get equipment manual from filesystem
/// </summary>
Task<(bool Success, byte[] FileContent, string ContentType, string ErrorMessage)> GetEquipmentManualAsync(string filePath);
/// <summary>
/// Check if equipment manual exists
/// </summary>
Task<bool> EquipmentManualExistsAsync(string filePath);
}
@@ -0,0 +1,48 @@
using Microsoft.AspNetCore.Http;
namespace PowderCoating.Application.Interfaces;
public interface IFileService
{
/// <summary>
/// Saves an uploaded file to the specified subfolder
/// </summary>
/// <param name="file">The file to save</param>
/// <param name="subfolder">Subfolder within uploads directory (e.g., "equipment-manuals")</param>
/// <param name="allowedExtensions">Array of allowed file extensions (e.g., [".pdf", ".docx"])</param>
/// <param name="maxFileSize">Maximum file size in bytes</param>
/// <returns>Tuple containing success status, file path (relative to wwwroot), and error message if any</returns>
Task<(bool Success, string FilePath, string ErrorMessage)> SaveFileAsync(
IFormFile file,
string subfolder,
string[] allowedExtensions,
long maxFileSize);
/// <summary>
/// Deletes a file from the file system
/// </summary>
/// <param name="filePath">Relative path to the file (e.g., "uploads/equipment-manuals/file.pdf")</param>
/// <returns>Tuple containing success status and error message if any</returns>
Task<(bool Success, string ErrorMessage)> DeleteFileAsync(string filePath);
/// <summary>
/// Retrieves a file from the file system
/// </summary>
/// <param name="filePath">Relative path to the file</param>
/// <returns>Tuple containing success status, file content, content type, and error message if any</returns>
Task<(bool Success, byte[] FileContent, string ContentType, string ErrorMessage)> GetFileAsync(string filePath);
/// <summary>
/// Checks if a file exists
/// </summary>
/// <param name="filePath">Relative path to the file</param>
/// <returns>True if file exists, false otherwise</returns>
bool FileExists(string filePath);
/// <summary>
/// Gets the MIME content type for a file based on its extension
/// </summary>
/// <param name="fileName">The file name</param>
/// <returns>MIME content type</returns>
string GetContentType(string fileName);
}
@@ -0,0 +1,14 @@
namespace PowderCoating.Application.Interfaces;
public interface IInAppNotificationService
{
// Company-scoped notification — shown to all users of that company
Task CreateAsync(int companyId, string title, string message, string notificationType,
string? link = null, int? quoteId = null, int? invoiceId = null, int? customerId = null);
// Platform notification — shown only to SuperAdmins
Task CreateForSuperAdminsAsync(string title, string message, string notificationType, string? link = null);
// Broadcast notification — one record per active company, shown to all tenant users
Task CreateForAllCompaniesAsync(string title, string message, string notificationType, string? link = null);
}
@@ -0,0 +1,42 @@
namespace PowderCoating.Application.Interfaces;
public class InventoryAiLookupResult
{
public bool Success { get; set; }
public string? ErrorMessage { get; set; }
// Identity
public string? Manufacturer { get; set; }
public string? ManufacturerPartNumber { get; set; }
public string? ColorName { get; set; }
public string? ColorCode { get; set; }
public string? Description { get; set; }
// Coating specs
public string? Finish { get; set; }
public decimal? CureTemperatureF { get; set; }
public int? CureTimeMinutes { get; set; }
public string? ColorFamilies { get; set; } // comma-separated e.g. "Green,Blue"
public bool? RequiresClearCoat { get; set; }
// Application properties
public decimal? CoverageSqFtPerLb { get; set; } // typical ~80-120 sq ft/lb
public decimal? TransferEfficiency { get; set; } // typical 50-75%
public decimal? UnitCostPerLb { get; set; } // price per lb/unit if found in search results
public string? VendorName { get; set; } // manufacturer/vendor name for dropdown matching
public string? SpecPageUrl { get; set; } // URL of the product page that was fetched
public string? Reasoning { get; set; } // brief explanation of what was found
}
public interface IInventoryAiLookupService
{
/// <summary>
/// Search the web for powder coating product info and use AI to extract structured data.
/// </summary>
Task<InventoryAiLookupResult> LookupAsync(
string? manufacturer,
string? colorName,
string? colorCode,
string? partNumber);
}
@@ -0,0 +1,45 @@
using Microsoft.AspNetCore.Http;
using PowderCoating.Core.Enums;
namespace PowderCoating.Application.Interfaces;
public interface IJobPhotoService
{
/// <summary>
/// Saves a job photo to filesystem using a GUID-based filename for security
/// Stores in [ContentRoot]/media/{companyId}/job-photos/{jobId}/{guid}.{ext}
/// </summary>
/// <param name="file">The photo file to upload</param>
/// <param name="jobId">The job's ID</param>
/// <param name="companyId">The company's ID</param>
/// <param name="caption">Optional caption/note for the photo</param>
/// <param name="photoType">Type of photo (Before, Progress, After, etc.)</param>
/// <returns>Tuple with success status, relative file path, and error message if any</returns>
Task<(bool Success, string FilePath, string ErrorMessage)> SaveJobPhotoAsync(
IFormFile file,
int jobId,
int companyId,
string? caption = null,
JobPhotoType photoType = JobPhotoType.Progress);
/// <summary>
/// Deletes a job photo from filesystem
/// </summary>
/// <param name="filePath">Relative path to the photo file</param>
/// <returns>Tuple with success status and error message if any</returns>
Task<(bool Success, string ErrorMessage)> DeleteJobPhotoAsync(string filePath);
/// <summary>
/// Gets a job photo
/// </summary>
/// <param name="filePath">Relative path to the photo</param>
/// <returns>Tuple with success status, file bytes, content type, and error message</returns>
Task<(bool Success, byte[] FileContent, string ContentType, string ErrorMessage)> GetJobPhotoAsync(string filePath);
/// <summary>
/// Checks if a job photo exists
/// </summary>
/// <param name="filePath">Relative path to the photo</param>
/// <returns>True if exists, false otherwise</returns>
Task<bool> JobPhotoExistsAsync(string filePath);
}

Some files were not shown because too many files have changed in this diff Show More