Initial commit
This commit is contained in:
@@ -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; } // 1–30 days overdue
|
||||
public decimal AgingDays31To60 { get; set; } // 31–60 days overdue
|
||||
public decimal AgingDays61To90 { get; set; } // 61–90 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; }
|
||||
}
|
||||
@@ -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 2–8.</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
Reference in New Issue
Block a user