Initial commit

This commit is contained in:
2026-04-23 21:38:24 -04:00
commit 63e12a9636
1762 changed files with 1672620 additions and 0 deletions
@@ -0,0 +1,311 @@
@model PowderCoating.Application.DTOs.Company.CreateCompanyDto
@{
ViewData["Title"] = "Create Company";
ViewData["PageIcon"] = "bi-building-add";
var planConfigs = ((IEnumerable<PowderCoating.Core.Entities.SubscriptionPlanConfig>)ViewBag.PlanConfigs)
.OrderBy(c => c.SortOrder).ToList();
}
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-10">
<div class="d-flex justify-content-end mb-4">
<a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Back to List
</a>
</div>
<div class="card shadow-sm">
<div class="card-body">
<form asp-action="Create" method="post">
<partial name="_ValidationSummary" />
<h5 class="card-title mb-3 pb-2 border-bottom">Company Information</h5>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label asp-for="CompanyName" class="form-label">Company Name *</label>
<input asp-for="CompanyName" class="form-control" placeholder="Enter company name" />
<span asp-validation-for="CompanyName" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="CompanyCode" class="form-label">Company Code</label>
<input asp-for="CompanyCode" class="form-control" placeholder="e.g., ABC" maxlength="10" />
<span asp-validation-for="CompanyCode" class="text-danger"></span>
<small class="form-text text-muted">Optional short code for the company</small>
</div>
</div>
<h5 class="card-title mb-3 pb-2 border-bottom">Primary Contact</h5>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label asp-for="PrimaryContactName" class="form-label">Contact Name *</label>
<input asp-for="PrimaryContactName" class="form-control" placeholder="Full name" />
<span asp-validation-for="PrimaryContactName" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="PrimaryContactEmail" class="form-label">Contact Email *</label>
<input asp-for="PrimaryContactEmail" class="form-control" type="email" placeholder="email@example.com" />
<span asp-validation-for="PrimaryContactEmail" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="Phone" class="form-label">Phone</label>
<input asp-for="Phone" class="form-control" placeholder="(555) 123-4567" />
<span asp-validation-for="Phone" class="text-danger"></span>
</div>
</div>
<h5 class="card-title mb-3 pb-2 border-bottom">Address</h5>
<div class="row g-3 mb-4">
<div class="col-12">
<label asp-for="Address" class="form-label">Street Address</label>
<input asp-for="Address" class="form-control" placeholder="123 Main Street" />
<span asp-validation-for="Address" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="City" class="form-label">City</label>
<input asp-for="City" class="form-control" placeholder="City" />
<span asp-validation-for="City" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="State" class="form-label">State</label>
<input asp-for="State" class="form-control" placeholder="CA" maxlength="2" />
<span asp-validation-for="State" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="ZipCode" class="form-label">Zip Code</label>
<input asp-for="ZipCode" class="form-control" placeholder="90210" />
<span asp-validation-for="ZipCode" class="text-danger"></span>
</div>
</div>
<h5 class="card-title mb-3 pb-2 border-bottom">Subscription Details</h5>
<div class="row g-3 mb-4">
<div class="col-md-4">
<label asp-for="SubscriptionPlan" class="form-label">Plan</label>
<select asp-for="SubscriptionPlan" class="form-select">
@foreach (var plan in planConfigs)
{
<option value="@plan.Plan">@plan.DisplayName</option>
}
</select>
<span asp-validation-for="SubscriptionPlan" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="SubscriptionStartDate" class="form-label">Start Date</label>
<input asp-for="SubscriptionStartDate" class="form-control" type="date" />
<span asp-validation-for="SubscriptionStartDate" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="SubscriptionEndDate" class="form-label">End Date</label>
<input asp-for="SubscriptionEndDate" class="form-control" type="date" />
<span asp-validation-for="SubscriptionEndDate" class="text-danger"></span>
<small class="form-text text-muted">Leave blank for ongoing</small>
</div>
<div class="col-md-6">
<label asp-for="TimeZone" class="form-label">Time Zone</label>
<select asp-for="TimeZone" class="form-select">
<option value="America/New_York">Eastern Time (ET)</option>
<option value="America/Chicago">Central Time (CT)</option>
<option value="America/Denver">Mountain Time (MT)</option>
<option value="America/Phoenix">Arizona Time (MT - No DST)</option>
<option value="America/Los_Angeles">Pacific Time (PT)</option>
<option value="America/Anchorage">Alaska Time (AKT)</option>
<option value="Pacific/Honolulu">Hawaii Time (HT)</option>
</select>
<span asp-validation-for="TimeZone" class="text-danger"></span>
</div>
<div class="col-md-6">
<div class="form-check form-switch mt-4">
<input asp-for="IsActive" class="form-check-input" type="checkbox" />
<label asp-for="IsActive" class="form-check-label">Company Active</label>
</div>
</div>
</div>
<h5 class="card-title mb-3 pb-2 border-bottom">Initial Admin User</h5>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label asp-for="AdminFirstName" class="form-label">First Name *</label>
<input asp-for="AdminFirstName" class="form-control" placeholder="First name" />
<span asp-validation-for="AdminFirstName" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="AdminLastName" class="form-label">Last Name *</label>
<input asp-for="AdminLastName" class="form-control" placeholder="Last name" />
<span asp-validation-for="AdminLastName" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="AdminEmail" class="form-label">Admin Email *</label>
<input asp-for="AdminEmail" class="form-control" type="email" placeholder="admin@company.com" />
<span asp-validation-for="AdminEmail" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="AdminPassword" class="form-label">Admin Password *</label>
<div class="input-group">
<input asp-for="AdminPassword" id="adminPassword" class="form-control" type="password"
placeholder="Enter strong password"
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@@$!%*?&#^()_+=\-\[\]{};':&quot;\\|,.<>\/`~]).{8,}$"
title="Password must be at least 8 characters and include uppercase, lowercase, number, and special character" />
<button class="btn btn-outline-secondary" type="button" id="togglePassword">
<i class="bi bi-eye" id="toggleIcon"></i>
</button>
</div>
<span asp-validation-for="AdminPassword" class="text-danger d-block"></span>
<!-- Password Strength Indicator -->
<div class="mt-2">
<div class="progress" style="height: 5px;">
<div id="passwordStrength" class="progress-bar" role="progressbar" style="width: 0%"></div>
</div>
<small id="passwordStrengthText" class="form-text"></small>
</div>
<!-- Password Requirements -->
<div class="mt-2">
<small class="form-text text-muted d-block mb-1"><strong>Password must contain:</strong></small>
<small class="form-text d-block" id="req-length">
<i class="bi bi-circle text-muted"></i> At least 8 characters
</small>
<small class="form-text d-block" id="req-uppercase">
<i class="bi bi-circle text-muted"></i> One uppercase letter (A-Z)
</small>
<small class="form-text d-block" id="req-lowercase">
<i class="bi bi-circle text-muted"></i> One lowercase letter (a-z)
</small>
<small class="form-text d-block" id="req-number">
<i class="bi bi-circle text-muted"></i> One number (0-9)
</small>
<small class="form-text d-block" id="req-special">
<i class="bi bi-circle text-muted"></i> One special character (!@@#$%^&amp;*)
</small>
</div>
</div>
</div>
<div class="d-flex gap-2 justify-content-end">
<a asp-action="Index" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary">
<i class="bi bi-save me-1"></i>Create Company
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
@{
await Html.RenderPartialAsync("_ValidationScriptsPartial");
}
<script>
// Password visibility toggle
document.getElementById('togglePassword').addEventListener('click', function() {
const passwordInput = document.getElementById('adminPassword');
const toggleIcon = document.getElementById('toggleIcon');
if (passwordInput.type === 'password') {
passwordInput.type = 'text';
toggleIcon.classList.remove('bi-eye');
toggleIcon.classList.add('bi-eye-slash');
} else {
passwordInput.type = 'password';
toggleIcon.classList.remove('bi-eye-slash');
toggleIcon.classList.add('bi-eye');
}
});
// Password validation and strength
document.getElementById('adminPassword').addEventListener('input', function() {
const password = this.value;
let strength = 0;
const requirements = {
length: password.length >= 8,
uppercase: /[A-Z]/.test(password),
lowercase: /[a-z]/.test(password),
number: /\d/.test(password),
special: /[@@$!%*?&#^()_+=\-\[\]{};':\"\\|,.<>\/`~]/.test(password)
};
// Update requirement indicators
updateRequirement('req-length', requirements.length);
updateRequirement('req-uppercase', requirements.uppercase);
updateRequirement('req-lowercase', requirements.lowercase);
updateRequirement('req-number', requirements.number);
updateRequirement('req-special', requirements.special);
// Calculate strength
if (requirements.length) strength += 20;
if (requirements.uppercase) strength += 20;
if (requirements.lowercase) strength += 20;
if (requirements.number) strength += 20;
if (requirements.special) strength += 20;
// Update strength bar
const strengthBar = document.getElementById('passwordStrength');
const strengthText = document.getElementById('passwordStrengthText');
strengthBar.style.width = strength + '%';
strengthBar.className = 'progress-bar';
if (strength === 0) {
strengthBar.classList.add('bg-secondary');
strengthText.textContent = '';
strengthText.className = 'form-text';
} else if (strength < 60) {
strengthBar.classList.add('bg-danger');
strengthText.textContent = 'Weak password';
strengthText.className = 'form-text text-danger';
} else if (strength < 100) {
strengthBar.classList.add('bg-warning');
strengthText.textContent = 'Fair password';
strengthText.className = 'form-text text-warning';
} else {
strengthBar.classList.add('bg-success');
strengthText.textContent = 'Strong password';
strengthText.className = 'form-text text-success';
}
});
function updateRequirement(elementId, met) {
const element = document.getElementById(elementId);
const icon = element.querySelector('i');
if (met) {
icon.classList.remove('bi-circle', 'text-muted');
icon.classList.add('bi-check-circle-fill', 'text-success');
element.classList.remove('text-muted');
element.classList.add('text-success');
} else {
icon.classList.remove('bi-check-circle-fill', 'text-success');
icon.classList.add('bi-circle', 'text-muted');
element.classList.remove('text-success');
element.classList.add('text-muted');
}
}
// Form submission validation
document.querySelector('form').addEventListener('submit', function(e) {
const password = document.getElementById('adminPassword').value;
const requirements = {
length: password.length >= 8,
uppercase: /[A-Z]/.test(password),
lowercase: /[a-z]/.test(password),
number: /\d/.test(password),
special: /[@@$!%*?&#^()_+=\-\[\]{};':\"\\|,.<>\/`~]/.test(password)
};
const allMet = Object.values(requirements).every(req => req === true);
if (!allMet) {
e.preventDefault();
showError('Please ensure the admin password meets all requirements before submitting.', 'Validation Error');
document.getElementById('adminPassword').focus();
return false;
}
});
</script>
}
@@ -0,0 +1,145 @@
@model PowderCoating.Application.DTOs.Company.CreateCompanyAdminDto
@{
ViewData["Title"] = "Create Company Admin";
ViewData["PageIcon"] = "bi-person-badge";
}
<div class="container-fluid">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="d-flex justify-content-end mb-4">
<a asp-action="Details" asp-route-id="@Model.CompanyId" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-2"></i>Back to Company
</a>
</div>
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<form asp-action="CreateCompanyAdmin" method="post">
<input type="hidden" asp-for="CompanyId" />
<input type="hidden" asp-for="CompanyName" />
<partial name="_ValidationSummary" />
<!-- Company Information -->
<div class="mb-4">
<h5 class="border-bottom pb-2 mb-3">
<i class="bi bi-building me-2 text-primary"></i>Company
</h5>
<div class="alert alert-info">
<i class="bi bi-info-circle me-2"></i>
Creating admin user for: <strong>@Model.CompanyName</strong>
</div>
</div>
<!-- Personal Information -->
<div class="mb-4">
<h5 class="border-bottom pb-2 mb-3">
<i class="bi bi-person me-2 text-primary"></i>Personal Information
</h5>
<div class="row g-3">
<div class="col-md-6">
<label asp-for="FirstName" class="form-label"></label>
<input asp-for="FirstName" class="form-control" placeholder="Enter first name" />
<span asp-validation-for="FirstName" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="LastName" class="form-label"></label>
<input asp-for="LastName" class="form-control" placeholder="Enter last name" />
<span asp-validation-for="LastName" class="text-danger"></span>
</div>
</div>
</div>
<!-- Contact Information -->
<div class="mb-4">
<h5 class="border-bottom pb-2 mb-3">
<i class="bi bi-envelope me-2 text-primary"></i>Contact & Account
</h5>
<div class="row g-3">
<div class="col-md-6">
<label asp-for="Email" class="form-label"></label>
<input asp-for="Email" type="email" class="form-control" placeholder="admin@example.com" />
<span asp-validation-for="Email" class="text-danger"></span>
<small class="text-muted">This will be the user's login email</small>
</div>
<div class="col-md-6">
<label asp-for="Phone" class="form-label"></label>
<input asp-for="Phone" type="tel" class="form-control" placeholder="(555) 123-4567" />
<span asp-validation-for="Phone" class="text-danger"></span>
</div>
</div>
</div>
<!-- Password -->
<div class="mb-4">
<h5 class="border-bottom pb-2 mb-3">
<i class="bi bi-key me-2 text-primary"></i>Password
</h5>
<div class="row g-3">
<div class="col-md-12">
<label asp-for="Password" class="form-label"></label>
<input asp-for="Password" class="form-control" placeholder="Enter a strong password" />
<span asp-validation-for="Password" class="text-danger"></span>
<small class="text-muted d-block mt-1">
Password must be at least 8 characters with uppercase, lowercase, digit, and special character
</small>
</div>
</div>
</div>
<!-- Work Information (Optional) -->
<div class="mb-4">
<h5 class="border-bottom pb-2 mb-3">
<i class="bi bi-briefcase me-2 text-primary"></i>Work Information (Optional)
</h5>
<div class="row g-3">
<div class="col-md-6">
<label asp-for="Department" class="form-label"></label>
<input asp-for="Department" class="form-control" placeholder="e.g., Management" />
<span asp-validation-for="Department" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="Position" class="form-label"></label>
<input asp-for="Position" class="form-control" placeholder="e.g., Company Administrator" />
<span asp-validation-for="Position" class="text-danger"></span>
</div>
</div>
</div>
<!-- Permissions Notice -->
<div class="alert alert-success mb-4">
<h6 class="alert-heading">
<i class="bi bi-shield-check me-2"></i>Administrator Permissions
</h6>
<p class="mb-0">
This user will be created with <strong>Company Administrator</strong> role and will have full permissions to:
</p>
<ul class="mb-0 mt-2">
<li>Manage all jobs, quotes, and customers</li>
<li>Manage inventory and equipment</li>
<li>Create and approve quotes</li>
<li>Manage company users and settings</li>
</ul>
</div>
<!-- Form Actions -->
<div class="d-flex gap-2 justify-content-end pt-3 border-top">
<a asp-action="Details" asp-route-id="@Model.CompanyId" class="btn btn-outline-secondary px-4">
Cancel
</a>
<button type="submit" class="btn btn-primary px-4">
<i class="bi bi-person-plus me-2"></i>Create Admin User
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
@@ -0,0 +1,893 @@
@model PowderCoating.Application.DTOs.Company.CompanyDto
@{
ViewData["Title"] = Model.CompanyName;
ViewData["PageIcon"] = "bi-building";
var planConfigs = ((IEnumerable<PowderCoating.Core.Entities.SubscriptionPlanConfig>)ViewBag.PlanConfigs)
.OrderBy(c => c.SortOrder).ToList();
var planBadgeColors = planConfigs.Select((c, i) => (c.Plan, i))
.ToDictionary(x => x.Plan, x => x.i switch { 0 => "bg-secondary", 1 => "bg-primary", 2 => "bg-info", _ => "bg-success" });
var planNames = planConfigs.ToDictionary(c => c.Plan, c => c.DisplayName);
string PlanBadge(int plan) => planBadgeColors.TryGetValue(plan, out var c) ? c : "bg-secondary";
string PlanName(int plan) => planNames.TryGetValue(plan, out var n) ? n : plan.ToString();
}
<div class="container-fluid">
<div class="d-flex justify-content-between align-items-center mb-4">
<div class="d-flex align-items-center gap-2">
@if (!string.IsNullOrEmpty(Model.CompanyCode))
{
<span class="badge bg-secondary">@Model.CompanyCode</span>
}
@if (Model.IsActive)
{
<span class="badge bg-success">Active</span>
}
else
{
<span class="badge bg-danger">Inactive</span>
}
</div>
<div class="btn-group">
<a asp-action="CreateCompanyAdmin" asp-route-id="@Model.Id" class="btn btn-success">
<i class="bi bi-person-plus me-1"></i>Add Admin User
</a>
<a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-primary">
<i class="bi bi-pencil me-1"></i>Edit
</a>
<a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Back to List
</a>
</div>
</div> @if (TempData["Warning"] != null)
{
<div class="alert alert-warning alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle me-2"></i>@TempData["Warning"]
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
} <!-- Statistics Cards - Desktop -->
<div class="stats-cards-desktop">
<div class="row g-3 mb-4">
<div class="col-md-3">
<div class="card shadow-sm">
<div class="card-body text-center">
<i class="bi bi-people text-primary" style="font-size: 2rem;"></i>
<h3 class="mt-2 mb-0">@Model.UserCount</h3>
<p class="text-muted small mb-0">Users</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card shadow-sm">
<div class="card-body text-center">
<i class="bi bi-person-badge text-success" style="font-size: 2rem;"></i>
<h3 class="mt-2 mb-0">@Model.CustomerCount</h3>
<p class="text-muted small mb-0">Customers</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card shadow-sm">
<div class="card-body text-center">
<i class="bi bi-briefcase text-info" style="font-size: 2rem;"></i>
<h3 class="mt-2 mb-0">@Model.JobCount</h3>
<p class="text-muted small mb-0">Jobs</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card shadow-sm">
<div class="card-body text-center">
<i class="bi bi-star text-warning" style="font-size: 2rem;"></i>
<h3 class="mt-2 mb-0">@PlanName(Model.SubscriptionPlan)</h3>
<p class="text-muted small mb-0">Plan</p>
</div>
</div>
</div>
</div>
</div>
<!-- Compact Stats - Mobile -->
<div class="mobile-stats-compact">
<div class="card">
<div class="stats-grid">
<div class="stat-item">
<div class="stat-icon"><i class="bi bi-people text-primary"></i></div>
<div class="stat-value">@Model.UserCount</div>
<div class="stat-label">Users</div>
</div>
<div class="stat-item">
<div class="stat-icon"><i class="bi bi-person-badge text-success"></i></div>
<div class="stat-value">@Model.CustomerCount</div>
<div class="stat-label">Customers</div>
</div>
<div class="stat-item">
<div class="stat-icon"><i class="bi bi-briefcase text-info"></i></div>
<div class="stat-value">@Model.JobCount</div>
<div class="stat-label">Jobs</div>
</div>
<div class="stat-item">
<div class="stat-icon"><i class="bi bi-star text-warning"></i></div>
<div class="stat-value">@PlanName(Model.SubscriptionPlan)</div>
<div class="stat-label">Plan</div>
</div>
</div>
</div>
</div>
<div class="row g-3">
<!-- Company Information -->
<div class="col-lg-6">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h5 class="card-title mb-0">
<i class="bi bi-info-circle me-2"></i>Company Information
</h5>
</div>
<div class="card-body">
<table class="table table-sm table-borderless">
<tr>
<th style="width: 40%;">Company Name:</th>
<td>@Model.CompanyName</td>
</tr>
<tr>
<th>Company Code:</th>
<td>@(Model.CompanyCode ?? "N/A")</td>
</tr>
<tr>
<th>Status:</th>
<td>
@if (Model.IsActive)
{
<span class="badge bg-success">Active</span>
}
else
{
<span class="badge bg-danger">Inactive</span>
}
</td>
</tr>
<tr>
<th>Time Zone:</th>
<td>@(Model.TimeZone ?? "America/New_York")</td>
</tr>
</table>
</div>
</div>
</div>
<!-- Contact Information -->
<div class="col-lg-6">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h5 class="card-title mb-0">
<i class="bi bi-person-lines-fill me-2"></i>Primary Contact
</h5>
</div>
<div class="card-body">
<table class="table table-sm table-borderless">
<tr>
<th style="width: 40%;">Contact Name:</th>
<td>@Model.PrimaryContactName</td>
</tr>
<tr>
<th>Email:</th>
<td><a href="mailto:@Model.PrimaryContactEmail">@Model.PrimaryContactEmail</a></td>
</tr>
<tr>
<th>Phone:</th>
<td>@(Model.Phone ?? "N/A")</td>
</tr>
</table>
</div>
</div>
</div>
<!-- Address -->
<div class="col-lg-6">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h5 class="card-title mb-0">
<i class="bi bi-geo-alt me-2"></i>Address
</h5>
</div>
<div class="card-body">
@if (!string.IsNullOrEmpty(Model.Address))
{
<address>
@Model.Address<br />
@if (!string.IsNullOrEmpty(Model.City) || !string.IsNullOrEmpty(Model.State) || !string.IsNullOrEmpty(Model.ZipCode))
{
<text>@Model.City, @Model.State @Model.ZipCode</text>
}
</address>
}
else
{
<p class="text-muted mb-0">No address provided</p>
}
</div>
</div>
</div>
<!-- Subscription -->
<div class="col-lg-6">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h5 class="card-title mb-0">
<i class="bi bi-credit-card me-2"></i>Subscription Details
</h5>
</div>
<div class="card-body">
<table class="table table-sm table-borderless">
<tr>
<th style="width: 40%;">Plan:</th>
<td>
<span class="badge @PlanBadge(Model.SubscriptionPlan)">@PlanName(Model.SubscriptionPlan)</span>
</td>
</tr>
<tr>
<th>Start Date:</th>
<td>@Model.SubscriptionStartDate.ToString("MMMM d, yyyy")</td>
</tr>
<tr>
<th>End Date:</th>
<td>
@if (Model.SubscriptionEndDate.HasValue)
{
@Model.SubscriptionEndDate.Value.ToString("MMMM d, yyyy")
}
else
{
<span class="text-muted">Ongoing</span>
}
</td>
</tr>
</table>
</div>
</div>
</div>
<!-- Audit Information -->
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h5 class="card-title mb-0">
<i class="bi bi-clock-history me-2"></i>Audit Information
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<table class="table table-sm table-borderless">
<tr>
<th style="width: 30%;">Created:</th>
<td>@Model.CreatedAt.ToString("MMMM d, yyyy h:mm tt")</td>
</tr>
@if (!string.IsNullOrEmpty(Model.CreatedBy))
{
<tr>
<th>Created By:</th>
<td>@Model.CreatedBy</td>
</tr>
}
</table>
</div>
<div class="col-md-6">
@if (Model.UpdatedAt.HasValue)
{
<table class="table table-sm table-borderless">
<tr>
<th style="width: 30%;">Updated:</th>
<td>@Model.UpdatedAt.Value.ToString("MMMM d, yyyy h:mm tt")</td>
</tr>
@if (!string.IsNullOrEmpty(Model.UpdatedBy))
{
<tr>
<th>Updated By:</th>
<td>@Model.UpdatedBy</td>
</tr>
}
</table>
}
</div>
</div>
</div>
</div>
</div>
<!-- Users List -->
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
<i class="bi bi-people me-2"></i>Users (@Model.Users.Count)
</h5>
<a asp-action="CreateCompanyAdmin" asp-route-id="@Model.Id" class="btn btn-sm btn-success">
<i class="bi bi-person-plus me-1"></i>Add Admin User
</a>
</div>
<div class="card-body p-0">
@if (Model.Users.Any())
{
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Name</th>
<th>Email</th>
<th>Role</th>
<th>Department</th>
<th>Status</th>
<th>Last Login</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var user in Model.Users.OrderBy(u => u.LastName))
{
<tr class="user-row" style="cursor:pointer;"
data-user-id="@user.Id"
data-company-id="@Model.Id"
title="Click to view user details and login history">
<td>
<strong>@user.FullName</strong>
@if (user.CompanyRole == null)
{
<span class="badge bg-warning text-dark ms-1" title="Platform User">
<i class="bi bi-star-fill"></i> SuperAdmin
</span>
}
</td>
<td>
<a href="mailto:@user.Email">@user.Email</a>
</td>
<td>
@if (!string.IsNullOrEmpty(user.CompanyRole))
{
<span class="badge @(user.CompanyRole switch
{
"CompanyAdmin" => "bg-primary",
"Manager" => "bg-info",
"Worker" => "bg-secondary",
_ => "bg-light text-dark"
})">
@user.CompanyRole.Replace("Company", "")
</span>
}
else
{
<span class="text-muted">N/A</span>
}
</td>
<td>@(user.Department ?? "N/A")</td>
<td>
@if (user.IsActive)
{
<span class="badge bg-success">Active</span>
}
else
{
<span class="badge bg-danger">Inactive</span>
}
</td>
<td>
@if (user.LastLoginDate.HasValue)
{
<small class="text-muted">@user.LastLoginDate.Value.ToString("MMM d, yyyy")</small>
}
else
{
<small class="text-muted">Never</small>
}
</td>
<td class="text-end" onclick="event.stopPropagation()">
@if (user.CompanyRole != null)
{
<div class="btn-group btn-group-sm" role="group">
<a asp-controller="CompanyUsers"
asp-action="Edit"
asp-route-id="@user.Id"
asp-route-returnUrl="@Url.Action("Details", "Companies", new { id = Model.Id })"
class="btn btn-outline-primary"
title="Edit User">
<i class="bi bi-pencil"></i>
</a>
<button type="button"
class="btn btn-outline-secondary"
data-bs-toggle="modal"
data-bs-target="#resetPasswordModal"
data-user-id="@user.Id"
data-user-name="@user.FullName"
title="Reset Password (set manually)">
<i class="bi bi-key"></i>
</button>
<form asp-controller="CompanyUsers"
asp-action="SendPasswordResetEmail"
asp-route-id="@user.Id"
method="post"
class="d-inline"
onsubmit="return confirm('Send a password reset link to @Html.Encode(user.Email)?')">
@Html.AntiForgeryToken()
<button type="submit"
class="btn btn-outline-info"
title="Email password reset link">
<i class="bi bi-envelope-arrow-up"></i>
</button>
</form>
<form asp-controller="CompanyUsers"
asp-action="ToggleActive"
asp-route-id="@user.Id"
method="post"
class="d-inline">
@Html.AntiForgeryToken()
<button type="submit"
class="btn btn-outline-@(user.IsActive ? "warning" : "success")"
title="@(user.IsActive ? "Deactivate" : "Activate")">
<i class="bi bi-@(user.IsActive ? "pause" : "play")"></i>
</button>
</form>
</div>
}
else
{
<span class="text-muted small">Platform User</span>
}
</td>
</tr>
}
</tbody>
</table>
</div>
}
else
{
<div class="text-center py-5">
<i class="bi bi-people" style="font-size: 3rem; color: #ccc;"></i>
<p class="text-muted mt-3 mb-3">No users found for this company.</p>
<a asp-action="CreateCompanyAdmin" asp-route-id="@Model.Id" class="btn btn-primary">
<i class="bi bi-person-plus me-1"></i>Create First Admin User
</a>
</div>
}
</div>
</div>
</div>
<!-- Actions -->
<div class="col-12">
<div class="card shadow-sm border-danger">
<div class="card-header bg-light">
<h5 class="card-title mb-0 text-danger">
<i class="bi bi-exclamation-triangle me-2"></i>Danger Zone
</h5>
</div>
<div class="card-body d-flex flex-column gap-3">
<div class="d-flex justify-content-between align-items-start border rounded p-3 bg-warning bg-opacity-10">
<div>
<h6 class="mb-1 text-warning-emphasis">
<i class="bi bi-fire me-1"></i>Reset All Company Data
</h6>
<p class="text-muted small mb-0">
Permanently deletes <strong>all business data</strong> — customers, vendors, accounts, invoices, bills, jobs, quotes, inventory, catalog items, and more.
The company record, user accounts, and system configuration are preserved.
Use this to wipe a migration and start fresh.
</p>
</div>
<button type="button" class="btn btn-warning ms-3 text-nowrap"
data-bs-toggle="modal" data-bs-target="#resetDataModal">
<i class="bi bi-fire me-1"></i>Reset Data
</button>
</div>
<div class="d-flex justify-content-between align-items-start border rounded p-3 bg-danger bg-opacity-10">
<div>
<h6 class="mb-1 text-danger">
<i class="bi bi-trash me-1"></i>Delete Company
</h6>
<p class="text-muted small mb-0">
Permanently deletes the company <strong>and everything in it</strong>, including all users.
There is no going back.
@if (Model.UserCount > 0)
{
<br /><strong class="text-danger">This company has @Model.UserCount user(s) — remove them first.</strong>
}
</p>
</div>
<button type="button" class="btn btn-danger ms-3 text-nowrap"
data-bs-toggle="modal" data-bs-target="#hardDeleteModal"
@(Model.UserCount > 0 ? "disabled" : "")>
<i class="bi bi-trash me-1"></i>Delete Company
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- User Details Modal -->
<div class="modal fade" tabindex="-1" id="userDetailOffcanvas" aria-labelledby="userDetailOffcanvasLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" style="max-width:520px;">
<div class="modal-content">
<div class="modal-header border-bottom">
<h5 class="modal-title" id="userDetailOffcanvasLabel">
<i class="bi bi-person-circle me-2"></i><span id="oc-name">User Details</span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-0">
<!-- Loading spinner -->
<div id="oc-loading" class="d-flex justify-content-center align-items-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading…</span>
</div>
</div>
<!-- Content (hidden until loaded) -->
<div id="oc-content" style="display:none;">
<!-- Profile card -->
<div class="px-3 pt-3 pb-2">
<div class="d-flex align-items-center gap-3 mb-3">
<div class="bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center flex-shrink-0"
style="width:52px;height:52px;">
<i class="bi bi-person-fill text-primary fs-4"></i>
</div>
<div>
<h6 class="mb-0 fw-semibold" id="oc-fullname"></h6>
<small class="text-muted" id="oc-email"></small>
</div>
<span id="oc-status-badge" class="badge ms-auto"></span>
</div>
<table class="table table-sm table-borderless mb-0 small">
<tr><th style="width:38%;" class="text-muted fw-normal">Role</th><td id="oc-role">—</td></tr>
<tr><th class="text-muted fw-normal">Department</th><td id="oc-dept">—</td></tr>
<tr><th class="text-muted fw-normal">Position</th><td id="oc-position">—</td></tr>
<tr><th class="text-muted fw-normal">Phone</th><td id="oc-phone">—</td></tr>
<tr><th class="text-muted fw-normal">Hired</th><td id="oc-hire">—</td></tr>
<tr><th class="text-muted fw-normal">Account created</th><td id="oc-created">—</td></tr>
<tr><th class="text-muted fw-normal">Last login</th><td id="oc-lastlogin">—</td></tr>
<tr><th class="text-muted fw-normal">Email confirmed</th><td id="oc-emailconf">—</td></tr>
</table>
</div>
<hr class="my-0" />
<!-- Login history -->
<div class="px-3 pt-3">
<h6 class="fw-semibold mb-2">
<i class="bi bi-clock-history me-1"></i>Login History
<span class="text-muted fw-normal small" id="oc-log-count"></span>
</h6>
</div>
<div id="oc-no-logs" class="px-3 pb-3 text-muted small" style="display:none;">
No login records found. (The audit log table may have been empty before the fix was applied.)
</div>
<div class="table-responsive" id="oc-log-table-wrap">
<table class="table table-sm table-hover mb-0 small">
<thead class="table-light sticky-top">
<tr>
<th>Time</th>
<th>Event</th>
<th>IP Address</th>
</tr>
</thead>
<tbody id="oc-log-body"></tbody>
</table>
</div>
</div><!-- /oc-content -->
<!-- Error state -->
<div id="oc-error" class="px-3 py-4 text-danger small" style="display:none;">
<i class="bi bi-exclamation-triangle me-1"></i><span id="oc-error-msg">Failed to load user data.</span>
</div>
</div>
</div><!-- /modal-content -->
</div><!-- /modal-dialog -->
</div>
<!-- Reset Data Modal -->
<div class="modal fade" id="resetDataModal" tabindex="-1" aria-labelledby="resetDataModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-warning">
<div class="modal-header bg-warning bg-opacity-10">
<h5 class="modal-title text-warning-emphasis" id="resetDataModalLabel">
<i class="bi bi-fire me-2"></i>Reset All Company Data
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-warning alert-permanent">
<strong>This will permanently delete:</strong>
<ul class="mb-0 mt-1">
<li>All customers, vendors, catalog items, and inventory</li>
<li>All chart of accounts entries</li>
<li>All invoices, bills, payments, and deposits</li>
<li>All jobs, quotes, and purchase orders</li>
<li>All equipment, shop workers, and maintenance records</li>
<li>All appointments, notifications, and related data</li>
</ul>
</div>
<p><strong>Preserved:</strong> Company record, user accounts, operating costs, preferences, and system lookup tables.</p>
<p class="mb-1">Type <strong>DELETE</strong> to confirm:</p>
<input type="text" id="resetDataConfirmInput" class="form-control" placeholder="Type DELETE here" autocomplete="off" />
</div>
<form asp-action="ResetData" asp-route-id="@Model.Id" method="post" id="resetDataForm">
@Html.AntiForgeryToken()
<input type="hidden" name="confirmation" id="resetDataConfirmHidden" />
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" id="btnResetDataConfirm" class="btn btn-warning" disabled>
<i class="bi bi-fire me-1"></i>Reset All Data
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Hard Delete Company Modal -->
<div class="modal fade" id="hardDeleteModal" tabindex="-1" aria-labelledby="hardDeleteModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-danger">
<div class="modal-header bg-danger bg-opacity-10">
<h5 class="modal-title text-danger" id="hardDeleteModalLabel">
<i class="bi bi-trash me-2"></i>Permanently Delete Company
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-danger alert-permanent">
<strong>This will permanently delete the company and everything in it:</strong> all data, all users, all configuration. This cannot be undone.
</div>
<p class="mb-1">Type <strong>DELETE</strong> to confirm:</p>
<input type="text" id="hardDeleteConfirmInput" class="form-control" placeholder="Type DELETE here" autocomplete="off" />
</div>
<form asp-action="HardDelete" asp-route-id="@Model.Id" method="post" id="hardDeleteForm">
@Html.AntiForgeryToken()
<input type="hidden" name="confirmation" id="hardDeleteConfirmHidden" />
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" id="btnHardDeleteConfirm" class="btn btn-danger" disabled>
<i class="bi bi-trash me-1"></i>Permanently Delete Company
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Reset Password Modal -->
<div class="modal fade" id="resetPasswordModal" tabindex="-1" aria-labelledby="resetPasswordModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="resetPasswordModalLabel">
<i class="bi bi-key me-2"></i>Reset Password
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="resetPasswordForm" asp-controller="CompanyUsers" asp-action="ResetPassword" method="post">
@Html.AntiForgeryToken()
<input type="hidden" id="resetUserId" name="id" />
<div class="modal-body">
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle me-2"></i>
You are about to reset the password for <strong id="resetUserName"></strong>.
</div>
<div class="mb-3">
<label for="newPassword" class="form-label">New Password</label>
<input type="password" class="form-control" id="newPassword" name="newPassword" required
minlength="8" placeholder="Enter new password">
<small class="text-muted">
Password must be at least 8 characters with uppercase, lowercase, digit, and special character
</small>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">
<i class="bi bi-key me-2"></i>Reset Password
</button>
</div>
</form>
</div>
</div>
</div>
@section Scripts {
<script>
// Reset Data Modal — enable submit only when user types "DELETE"
(function () {
var input = document.getElementById('resetDataConfirmInput');
var hidden = document.getElementById('resetDataConfirmHidden');
var btn = document.getElementById('btnResetDataConfirm');
if (input) {
input.addEventListener('input', function () {
var match = input.value.trim() === 'DELETE';
btn.disabled = !match;
hidden.value = input.value.trim();
});
document.getElementById('resetDataModal').addEventListener('hidden.bs.modal', function () {
input.value = '';
hidden.value = '';
btn.disabled = true;
});
}
})();
// Hard Delete Modal — enable submit only when user types "DELETE"
(function () {
var input = document.getElementById('hardDeleteConfirmInput');
var hidden = document.getElementById('hardDeleteConfirmHidden');
var btn = document.getElementById('btnHardDeleteConfirm');
if (input) {
input.addEventListener('input', function () {
var match = input.value.trim() === 'DELETE';
btn.disabled = !match;
hidden.value = input.value.trim();
});
document.getElementById('hardDeleteModal').addEventListener('hidden.bs.modal', function () {
input.value = '';
hidden.value = '';
btn.disabled = true;
});
}
})();
// Reset Password Modal
var resetPasswordModal = document.getElementById('resetPasswordModal');
if (resetPasswordModal) {
resetPasswordModal.addEventListener('show.bs.modal', function (event) {
var button = event.relatedTarget;
var userId = button.getAttribute('data-user-id');
var userName = button.getAttribute('data-user-name');
var modalUserIdInput = resetPasswordModal.querySelector('#resetUserId');
var modalUserName = resetPasswordModal.querySelector('#resetUserName');
modalUserIdInput.value = userId;
modalUserName.textContent = userName;
});
}
// ── User Details Modal ────────────────────────────────────────────────
(function () {
const offcanvasEl = document.getElementById('userDetailOffcanvas');
const oc = new bootstrap.Modal(offcanvasEl);
const loading = document.getElementById('oc-loading');
const content = document.getElementById('oc-content');
const errorDiv = document.getElementById('oc-error');
const errorMsg = document.getElementById('oc-error-msg');
const actionBadgeClass = {
'Login': 'bg-success',
'Login2FABypassed': 'bg-success',
'FailedLogin': 'bg-danger',
'LoginDenied': 'bg-warning text-dark',
'AccountLockedOut': 'bg-danger',
};
const actionLabel = {
'Login': 'Login',
'Login2FABypassed': 'Login (2FA bypassed)',
'FailedLogin': 'Failed login',
'LoginDenied': 'Login denied',
'AccountLockedOut': 'Account locked out',
'SelfServiceAccountDeletion': 'Account deleted',
};
/// Shows the loading spinner and hides all other states.
function showLoading() {
loading.classList.remove('d-none');
content.style.display = 'none';
errorDiv.style.display = 'none';
}
/// Populates the offcanvas with the fetched user data and login history.
function renderUser(data) {
const u = data.user;
document.getElementById('oc-name').textContent = u.fullName;
document.getElementById('oc-fullname').textContent = u.fullName;
document.getElementById('oc-email').textContent = u.email;
const badge = document.getElementById('oc-status-badge');
badge.textContent = u.isActive ? 'Active' : 'Inactive';
badge.className = 'badge ms-auto ' + (u.isActive ? 'bg-success' : 'bg-danger');
document.getElementById('oc-role').textContent = u.companyRole ? u.companyRole.replace('Company', '') : '—';
document.getElementById('oc-dept').textContent = u.department || '—';
document.getElementById('oc-position').textContent = u.position || '—';
document.getElementById('oc-phone').textContent = u.phone || '—';
document.getElementById('oc-hire').textContent = u.hireDate || '—';
document.getElementById('oc-created').textContent = u.createdAt || '—';
document.getElementById('oc-lastlogin').textContent = u.lastLoginDate || 'Never';
document.getElementById('oc-emailconf').innerHTML = u.emailConfirmed
? '<span class="text-success"><i class="bi bi-check-circle-fill me-1"></i>Yes</span>'
: '<span class="text-warning"><i class="bi bi-x-circle-fill me-1"></i>No</span>';
// Login history table
const tbody = document.getElementById('oc-log-body');
const noLogs = document.getElementById('oc-no-logs');
const logWrap = document.getElementById('oc-log-table-wrap');
const countEl = document.getElementById('oc-log-count');
const logs = data.loginHistory;
tbody.innerHTML = '';
if (!logs || logs.length === 0) {
noLogs.style.display = '';
logWrap.style.display = 'none';
countEl.textContent = '';
} else {
noLogs.style.display = 'none';
logWrap.style.display = '';
countEl.textContent = `(${logs.length})`;
logs.forEach(function (log) {
const badgeClass = actionBadgeClass[log.action] || 'bg-secondary';
const label = actionLabel[log.action] || log.action;
const noteHtml = log.note ? `<br><span class="text-muted">${escHtml(log.note)}</span>` : '';
const tr = document.createElement('tr');
tr.innerHTML = `
<td class="text-nowrap">${escHtml(log.timestamp)}</td>
<td><span class="badge ${badgeClass}">${escHtml(label)}</span>${noteHtml}</td>
<td class="text-nowrap text-muted">${escHtml(log.ipAddress)}</td>`;
tbody.appendChild(tr);
});
}
loading.classList.add('d-none');
content.style.display = '';
}
function showError(msg) {
loading.classList.add('d-none');
content.style.display = 'none';
errorMsg.textContent = msg;
errorDiv.style.display = '';
}
function escHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// Wire up every user row
document.querySelectorAll('.user-row').forEach(function (row) {
row.addEventListener('click', function () {
const userId = row.dataset.userId;
const companyId = row.dataset.companyId;
showLoading();
oc.show();
fetch(`/Companies/UserLoginHistory?companyId=${encodeURIComponent(companyId)}&userId=${encodeURIComponent(userId)}`)
.then(function (resp) {
if (!resp.ok) throw new Error('Server returned ' + resp.status);
return resp.json();
})
.then(renderUser)
.catch(function (err) {
showError('Failed to load user data. ' + err.message);
});
});
});
})();
</script>
}
@@ -0,0 +1,189 @@
@model PowderCoating.Application.DTOs.Company.UpdateCompanyDto
@{
ViewData["Title"] = "Edit Company";
ViewData["PageIcon"] = "bi-building-gear";
var planConfigs = ((IEnumerable<PowderCoating.Core.Entities.SubscriptionPlanConfig>)ViewBag.PlanConfigs)
.OrderBy(c => c.SortOrder).ToList();
}
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-10">
<div class="d-flex justify-content-end mb-4">
<a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Back to List
</a>
</div>
<div class="card shadow-sm">
<div class="card-body">
<form asp-action="Edit" method="post">
<input type="hidden" asp-for="Id" />
<partial name="_ValidationSummary" />
<h5 class="card-title mb-3 pb-2 border-bottom">Company Information</h5>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label asp-for="CompanyName" class="form-label">Company Name *</label>
<input asp-for="CompanyName" class="form-control" />
<span asp-validation-for="CompanyName" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="CompanyCode" class="form-label">Company Code</label>
<input asp-for="CompanyCode" class="form-control" maxlength="10" />
<span asp-validation-for="CompanyCode" class="text-danger"></span>
</div>
</div>
<h5 class="card-title mb-3 pb-2 border-bottom">Primary Contact</h5>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label asp-for="PrimaryContactName" class="form-label">Contact Name *</label>
<input asp-for="PrimaryContactName" class="form-control" />
<span asp-validation-for="PrimaryContactName" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="PrimaryContactEmail" class="form-label">Contact Email *</label>
<input asp-for="PrimaryContactEmail" class="form-control" type="email" />
<span asp-validation-for="PrimaryContactEmail" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="Phone" class="form-label">Phone</label>
<input asp-for="Phone" class="form-control" />
<span asp-validation-for="Phone" class="text-danger"></span>
</div>
</div>
<h5 class="card-title mb-3 pb-2 border-bottom">Address</h5>
<div class="row g-3 mb-4">
<div class="col-12">
<label asp-for="Address" class="form-label">Street Address</label>
<input asp-for="Address" class="form-control" />
<span asp-validation-for="Address" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="City" class="form-label">City</label>
<input asp-for="City" class="form-control" />
<span asp-validation-for="City" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="State" class="form-label">State</label>
<input asp-for="State" class="form-control" maxlength="2" />
<span asp-validation-for="State" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="ZipCode" class="form-label">Zip Code</label>
<input asp-for="ZipCode" class="form-control" />
<span asp-validation-for="ZipCode" class="text-danger"></span>
</div>
</div>
<h5 class="card-title mb-3 pb-2 border-bottom">Subscription Details</h5>
<div class="row g-3 mb-4">
<div class="col-md-4">
<label asp-for="SubscriptionPlan" class="form-label">Plan</label>
<select asp-for="SubscriptionPlan" class="form-select">
@foreach (var plan in planConfigs)
{
<option value="@plan.Plan">@plan.DisplayName</option>
}
</select>
<span asp-validation-for="SubscriptionPlan" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="SubscriptionStartDate" class="form-label">Start Date</label>
<input asp-for="SubscriptionStartDate" class="form-control" type="date" />
<span asp-validation-for="SubscriptionStartDate" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="SubscriptionEndDate" class="form-label">End Date</label>
<input asp-for="SubscriptionEndDate" class="form-control" type="date" />
<span asp-validation-for="SubscriptionEndDate" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="TimeZone" class="form-label">Time Zone</label>
<select asp-for="TimeZone" class="form-select">
<option value="America/New_York">Eastern Time (ET)</option>
<option value="America/Chicago">Central Time (CT)</option>
<option value="America/Denver">Mountain Time (MT)</option>
<option value="America/Phoenix">Arizona Time (MT - No DST)</option>
<option value="America/Los_Angeles">Pacific Time (PT)</option>
<option value="America/Anchorage">Alaska Time (AKT)</option>
<option value="Pacific/Honolulu">Hawaii Time (HT)</option>
</select>
<span asp-validation-for="TimeZone" class="text-danger"></span>
</div>
<div class="col-md-6">
<div class="form-check form-switch mt-4">
<input asp-for="IsActive" class="form-check-input" type="checkbox" />
<label asp-for="IsActive" class="form-check-label">Company Active</label>
</div>
</div>
</div>
<h5 class="card-title mb-3 pb-2 border-bottom">Feature Overrides</h5>
<p class="text-muted small mb-3">Override plan-level feature access for this company. Leave blank (—) to inherit from the subscription plan.</p>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label fw-medium">Online Payments</label>
<select asp-for="OnlinePaymentsOverride" class="form-select">
<option value="">— Use plan default —</option>
<option value="true">Force Enable</option>
<option value="false">Force Disable</option>
</select>
<div class="form-text">Stripe Connect invoice payments. Still requires the company to connect their Stripe account.</div>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Accounting Module</label>
<select asp-for="AccountingOverride" class="form-select">
<option value="">— Use plan default —</option>
<option value="true">Force Enable</option>
<option value="false">Force Disable</option>
</select>
<div class="form-text">Chart of Accounts, Bills, Expenses, and Accounting Export.</div>
</div>
</div>
<h5 class="card-title mb-3 pb-2 border-bottom">AI Features</h5>
<p class="text-muted small mb-3">Control which AI-powered features are available to this company and set monthly usage limits.</p>
<div class="row g-3 mb-4">
<div class="col-md-4">
<div class="form-check form-switch">
<input asp-for="AiPhotoQuotesEnabled" class="form-check-input" type="checkbox" />
<label asp-for="AiPhotoQuotesEnabled" class="form-check-label fw-medium">AI Photo Quotes</label>
</div>
<div class="form-text">Allow this company to use photo-based AI quoting.</div>
</div>
<div class="col-md-4">
<div class="form-check form-switch">
<input asp-for="AiInventoryAssistEnabled" class="form-check-input" type="checkbox" />
<label asp-for="AiInventoryAssistEnabled" class="form-check-label fw-medium">AI Inventory Assist</label>
</div>
<div class="form-text">Allow this company to use AI lookup on inventory items.</div>
</div>
<div class="col-md-4">
<label asp-for="MaxAiPhotoQuotesPerMonthOverride" class="form-label">Monthly AI Quote Limit Override</label>
<input asp-for="MaxAiPhotoQuotesPerMonthOverride" type="number" class="form-control" min="-1" placeholder="Leave blank to use plan default" />
<div class="form-text">-1 = unlimited. 0 = disabled. Blank = use subscription plan default.</div>
</div>
</div>
<div class="d-flex gap-2 justify-content-end">
<a asp-action="Index" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary">
<i class="bi bi-save me-1"></i>Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
@{
await Html.RenderPartialAsync("_ValidationScriptsPartial");
}
}
@@ -0,0 +1,477 @@
@model List<PowderCoating.Application.DTOs.Company.CompanyListDto>
@section Styles {
<style>
[data-bs-theme="dark"] a.text-dark { color: var(--bs-body-color) !important; }
[data-bs-theme="dark"] .bi-building[style*="color:#ccc"] { color: var(--bs-secondary-color) !important; }
</style>
}
@{
ViewData["Title"] = "Companies";
ViewData["PageIcon"] = "bi-building";
var planConfigs = ((IEnumerable<PowderCoating.Core.Entities.SubscriptionPlanConfig>)ViewBag.PlanConfigs)
.OrderBy(c => c.SortOrder).ToList();
var planBadgeColors = planConfigs.Select((c, i) => (c.Plan, i))
.ToDictionary(x => x.Plan, x => x.i switch { 0 => "bg-secondary", 1 => "bg-primary", 2 => "bg-info", _ => "bg-success" });
var planNames = planConfigs.ToDictionary(c => c.Plan, c => c.DisplayName);
string PlanBadge(int plan) => planBadgeColors.TryGetValue(plan, out var c) ? c : "bg-secondary";
string PlanName(int plan) => planNames.TryGetValue(plan, out var n) ? n : plan.ToString();
var searchTerm = (string?)ViewBag.SearchTerm;
var sortColumn = (string)(ViewBag.SortColumn ?? "CompanyName");
var sortDirection = (string)(ViewBag.SortDirection ?? "asc");
var pageNumber = (int)(ViewBag.PageNumber ?? 1);
var pageSize = (int)(ViewBag.PageSize ?? 25);
var totalPages = (int)(ViewBag.TotalPages ?? 1);
var totalCount = (int)(ViewBag.TotalCount ?? 0);
var impersonatingId = (int?)(ViewBag.ImpersonatingCompanyId);
string SortLink(string col)
{
var dir = (sortColumn == col && sortDirection == "asc") ? "desc" : "asc";
return Url.Action("Index", new { searchTerm, sortColumn = col, sortDirection = dir, pageNumber = 1, pageSize })!;
}
string SortIcon(string col)
{
if (sortColumn != col) return "bi-chevron-expand text-muted";
return sortDirection == "asc" ? "bi-chevron-up" : "bi-chevron-down";
}
}
<div class="container-fluid">
<div class="d-flex justify-content-end mb-4">
<a asp-action="Create" class="btn btn-primary">
<i class="bi bi-plus-circle me-1"></i>Create New Company
</a>
</div>
<!-- Search -->
<div class="card shadow-sm mb-3">
<div class="card-body py-2">
<form asp-action="Index" method="get" class="row g-2 align-items-end">
<input type="hidden" name="sortColumn" value="@sortColumn" />
<input type="hidden" name="sortDirection" value="@sortDirection" />
<input type="hidden" name="pageSize" value="@pageSize" />
<div class="col-md-6">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" name="searchTerm" class="form-control"
placeholder="Search by name, code, email, phone…"
value="@searchTerm" />
</div>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary"><i class="bi bi-search"></i></button>
@if (!string.IsNullOrWhiteSpace(searchTerm))
{
<a asp-action="Index" asp-route-sortColumn="@sortColumn"
asp-route-sortDirection="@sortDirection" asp-route-pageSize="@pageSize"
class="btn btn-outline-secondary ms-1">Clear</a>
}
</div>
</form>
</div>
</div>
<div class="card shadow-sm">
<div class="card-body p-0">
@if (Model != null && Model.Any())
{
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>
<a href="@SortLink("CompanyName")" class="text-decoration-none text-dark d-flex align-items-center gap-1">
Company Name <i class="bi @SortIcon("CompanyName")"></i>
</a>
</th>
<th>Code</th>
<th>Contact Email</th>
<th>Phone</th>
<th>
<a href="@SortLink("Plan")" class="text-decoration-none text-dark d-flex align-items-center gap-1">
Plan <i class="bi @SortIcon("Plan")"></i>
</a>
</th>
<th>Users</th>
<th>Setup Wizard</th>
<th>
<a href="@SortLink("Status")" class="text-decoration-none text-dark d-flex align-items-center gap-1">
Status <i class="bi @SortIcon("Status")"></i>
</a>
</th>
<th>
<a href="@SortLink("Created")" class="text-decoration-none text-dark d-flex align-items-center gap-1">
Created <i class="bi @SortIcon("Created")"></i>
</a>
</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var company in Model)
{
var isImpersonating = impersonatingId.HasValue && impersonatingId.Value == company.Id;
var detailsUrl = Url.Action("Details", new { id = company.Id });
<tr class="@(isImpersonating ? "table-warning" : "")" style="cursor:pointer"
onclick="window.location='@detailsUrl'">
<td>
<strong>@company.CompanyName</strong>
@if (isImpersonating)
{
<span class="badge bg-warning text-dark ms-1">
<i class="bi bi-eye-fill me-1"></i>Active
</span>
}
</td>
<td>
@if (!string.IsNullOrEmpty(company.CompanyCode))
{
<span class="badge bg-secondary">@company.CompanyCode</span>
}
</td>
<td>@company.PrimaryContactEmail</td>
<td>@company.Phone</td>
<td>
<span class="badge @PlanBadge(company.SubscriptionPlan)">@PlanName(company.SubscriptionPlan)</span>
</td>
<td>
<span class="badge bg-primary rounded-pill">@company.UserCount</span>
</td>
<td>
@if (company.WizardCompleted)
{
var tooltip = company.WizardCompletedByName != null
? $"Completed by {company.WizardCompletedByName}"
+ (company.WizardCompletedAt.HasValue
? $" on {company.WizardCompletedAt.Value.Tz(ViewBag.CompanyTimeZone as string):MMM d, yyyy}"
: "")
: "Completed";
<span class="badge bg-success" title="@tooltip" data-bs-toggle="tooltip">
<i class="bi bi-check-circle-fill me-1"></i>Done
</span>
}
else
{
<span class="badge bg-light text-muted border">
<i class="bi bi-hourglass me-1"></i>Pending
</span>
}
</td>
<td>
@if (company.IsActive)
{
<span class="badge bg-success">Active</span>
}
else
{
<span class="badge bg-danger">Inactive</span>
}
</td>
<td>
<small class="text-muted">@company.CreatedAt.ToString("MMM d, yyyy")</small>
</td>
<td class="text-end" onclick="event.stopPropagation()">
<div class="btn-group btn-group-sm" role="group">
<a asp-action="Details" asp-route-id="@company.Id"
class="btn btn-outline-primary" title="View Details">
<i class="bi bi-eye"></i>
</a>
<a asp-action="Edit" asp-route-id="@company.Id"
class="btn btn-outline-secondary" title="Edit">
<i class="bi bi-pencil"></i>
</a>
<form asp-action="ToggleActive" asp-route-id="@company.Id"
method="post" class="d-inline">
@Html.AntiForgeryToken()
<button type="submit"
class="btn @(company.IsActive ? "btn-outline-warning" : "btn-outline-success")"
title="@(company.IsActive ? "Deactivate" : "Activate")">
<i class="bi bi-@(company.IsActive ? "pause" : "play")"></i>
</button>
</form>
@if (isImpersonating)
{
<form asp-action="StopImpersonating" method="post" class="d-inline">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-warning" title="Stop Impersonating">
<i class="bi bi-x-circle"></i>
</button>
</form>
}
else
{
<form asp-action="StartImpersonating" method="post" class="d-inline">
@Html.AntiForgeryToken()
<input type="hidden" name="companyId" value="@company.Id" />
<button type="submit" class="btn btn-outline-dark" title="Impersonate this company">
<i class="bi bi-person-fill-gear"></i>
</button>
</form>
}
<button type="button"
class="btn btn-outline-danger"
title="Delete Company"
onclick="event.stopPropagation(); openDeleteModal(@company.Id, '@Html.Raw(company.CompanyName.Replace("'", "\\'"))', @company.UserCount, @company.JobCount, @company.QuoteCount, @company.CustomerCount)">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
<!-- Mobile card view — shown on screens < 992px (table-responsive hidden by mobile-cards.css) -->
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var company in Model)
{
var isImpersonating = impersonatingId.HasValue && impersonatingId.Value == company.Id;
<a href="@Url.Action("Details", new { id = company.Id })" class="mobile-data-card text-decoration-none">
<div class="mobile-card-header">
<div class="mobile-card-icon bg-primary"><i class="bi bi-building"></i></div>
<div class="mobile-card-title">
<h6>@company.CompanyName</h6>
<small class="text-muted">@company.PrimaryContactEmail</small>
</div>
@if (company.IsActive)
{
<span class="badge bg-success">Active</span>
}
else
{
<span class="badge bg-danger">Inactive</span>
}
</div>
<div class="mobile-card-body">
<div class="mobile-card-row">
<span class="mobile-card-label">Plan</span>
<span class="mobile-card-value"><span class="badge @PlanBadge(company.SubscriptionPlan)">@PlanName(company.SubscriptionPlan)</span></span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Users</span>
<span class="mobile-card-value"><span class="badge bg-primary rounded-pill">@company.UserCount</span></span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Created</span>
<span class="mobile-card-value">@company.CreatedAt.ToString("MMM d, yyyy")</span>
</div>
@if (!string.IsNullOrEmpty(company.CompanyCode))
{
<div class="mobile-card-row">
<span class="mobile-card-label">Code</span>
<span class="mobile-card-value"><span class="badge bg-secondary">@company.CompanyCode</span></span>
</div>
}
</div>
<div class="mobile-card-footer">
<span class="btn btn-sm btn-outline-primary">View →</span>
</div>
</a>
}
</div>
</div>
<!-- Pagination -->
@if (totalPages > 1)
{
<div class="card-footer d-flex justify-content-between align-items-center">
<div class="text-muted small">
Showing @((pageNumber - 1) * pageSize + 1)@(Math.Min(pageNumber * pageSize, totalCount)) of @totalCount companies
</div>
<div class="d-flex align-items-center gap-3">
<div>
<select class="form-select form-select-sm" onchange="changePageSize(this.value)">
@foreach (var size in new[] { 10, 25, 50, 100 })
{
<option value="@size" selected="@(pageSize == size)">@size per page</option>
}
</select>
</div>
<nav>
<ul class="pagination pagination-sm mb-0">
<li class="page-item @(pageNumber == 1 ? "disabled" : "")">
<a class="page-link" href="@Url.Action("Index", new { searchTerm, sortColumn, sortDirection, pageNumber = pageNumber - 1, pageSize })">
<i class="bi bi-chevron-left"></i>
</a>
</li>
@for (int p = Math.Max(1, pageNumber - 2); p <= Math.Min(totalPages, pageNumber + 2); p++)
{
<li class="page-item @(p == pageNumber ? "active" : "")">
<a class="page-link" href="@Url.Action("Index", new { searchTerm, sortColumn, sortDirection, pageNumber = p, pageSize })">@p</a>
</li>
}
<li class="page-item @(pageNumber == totalPages ? "disabled" : "")">
<a class="page-link" href="@Url.Action("Index", new { searchTerm, sortColumn, sortDirection, pageNumber = pageNumber + 1, pageSize })">
<i class="bi bi-chevron-right"></i>
</a>
</li>
</ul>
</nav>
</div>
</div>
}
}
else
{
<div class="text-center py-5">
<i class="bi bi-building text-secondary" style="font-size: 3rem;"></i>
<p class="text-muted mt-3">
@if (!string.IsNullOrWhiteSpace(searchTerm))
{
<text>No companies match "<strong>@searchTerm</strong>".</text>
}
else
{
<text>No companies found.</text>
}
</p>
@if (string.IsNullOrWhiteSpace(searchTerm))
{
<a asp-action="Create" class="btn btn-primary">
<i class="bi bi-plus-circle me-1"></i>Create Your First Company
</a>
}
</div>
}
</div>
</div>
</div>
<!-- Delete Company Modal -->
<div class="modal fade" id="deleteCompanyModal" tabindex="-1" aria-labelledby="deleteCompanyModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content border-0 shadow-lg">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title" id="deleteCompanyModalLabel">
<i class="bi bi-exclamation-triangle-fill me-2"></i>Delete Company
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4">
<p class="mb-3">
You are about to delete <strong id="modal-company-name"></strong>. This company has the following data:
</p>
<div class="row g-3 mb-4">
<div class="col-3">
<div class="card border text-center p-2">
<div class="fs-4 fw-bold text-primary" id="modal-user-count">0</div>
<small class="text-muted">Users</small>
</div>
</div>
<div class="col-3">
<div class="card border text-center p-2">
<div class="fs-4 fw-bold text-success" id="modal-job-count">0</div>
<small class="text-muted">Jobs</small>
</div>
</div>
<div class="col-3">
<div class="card border text-center p-2">
<div class="fs-4 fw-bold text-info" id="modal-quote-count">0</div>
<small class="text-muted">Quotes</small>
</div>
</div>
<div class="col-3">
<div class="card border text-center p-2">
<div class="fs-4 fw-bold text-warning" id="modal-customer-count">0</div>
<small class="text-muted">Customers</small>
</div>
</div>
</div>
<hr />
<!-- Soft Delete Section -->
<div class="mb-4">
<h6 class="fw-bold text-warning"><i class="bi bi-pause-circle me-2"></i>Soft Delete (Deactivate)</h6>
<p class="text-muted small mb-2">
The company and all users will be <strong>deactivated</strong> but data is preserved.
This is reversible and can be undone by an administrator.
</p>
<form id="softDeleteForm" method="post">
@Html.AntiForgeryToken()
<input type="hidden" name="id" id="soft-delete-id" />
<button type="submit" class="btn btn-warning">
<i class="bi bi-pause-circle me-1"></i>Deactivate Company
</button>
</form>
</div>
<hr />
<!-- Hard Delete Section -->
<div>
<h6 class="fw-bold text-danger"><i class="bi bi-fire me-2"></i>Hard Delete (Permanent)</h6>
<p class="text-muted small mb-2">
<strong class="text-danger">This cannot be undone.</strong>
All company data — users, jobs, quotes, customers, invoices, and everything else — will be
<strong>permanently and irreversibly deleted</strong> from the database.
</p>
<div class="alert alert-danger py-2 mb-3">
<i class="bi bi-exclamation-octagon-fill me-2"></i>
Type <strong>DELETE</strong> below to enable permanent deletion.
</div>
<div class="mb-3">
<input type="text"
id="hard-delete-confirmation"
class="form-control"
placeholder="Type DELETE to confirm"
autocomplete="off"
oninput="validateHardDelete(this.value)" />
</div>
<form id="hardDeleteForm" method="post">
@Html.AntiForgeryToken()
<input type="hidden" name="id" id="hard-delete-id" />
<input type="hidden" name="confirmation" value="DELETE" />
<button type="submit" id="hard-delete-btn" class="btn btn-danger" disabled>
<i class="bi bi-trash-fill me-1"></i>Permanently Delete Everything
</button>
</form>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
</div>
</div>
</div>
</div>
@section Scripts {
<script>
function changePageSize(size) {
const url = new URL(window.location.href);
url.searchParams.set('pageSize', size);
url.searchParams.set('pageNumber', '1');
window.location.href = url.toString();
}
function openDeleteModal(id, name, users, jobs, quotes, customers) {
document.getElementById('modal-company-name').textContent = name;
document.getElementById('modal-user-count').textContent = users;
document.getElementById('modal-job-count').textContent = jobs;
document.getElementById('modal-quote-count').textContent = quotes;
document.getElementById('modal-customer-count').textContent = customers;
document.getElementById('soft-delete-id').value = id;
document.getElementById('hard-delete-id').value = id;
document.getElementById('softDeleteForm').action = '/Companies/SoftDelete/' + id;
document.getElementById('hardDeleteForm').action = '/Companies/HardDelete/' + id;
// Reset hard delete input
document.getElementById('hard-delete-confirmation').value = '';
document.getElementById('hard-delete-btn').disabled = true;
new bootstrap.Modal(document.getElementById('deleteCompanyModal')).show();
}
function validateHardDelete(value) {
document.getElementById('hard-delete-btn').disabled = (value !== 'DELETE');
}
</script>
}