Initial commit
This commit is contained in:
@@ -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)(?=.*[@@$!%*?&#^()_+=\-\[\]{};':"\\|,.<>\/`~]).{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 (!@@#$%^&*)
|
||||
</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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// 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>
|
||||
}
|
||||
Reference in New Issue
Block a user