From c53b128bebd379405dc2e4e3662f33ec31f4cb42 Mon Sep 17 00:00:00 2001 From: Tom Karasch Date: Thu, 20 Aug 2026 11:40:34 -0400 Subject: [PATCH 1/3] Implement dynamic reporting period dates and improve form feedback Introduces a new script to automatically calculate and populate reporting period start and end dates based on user selection. Enhances visual feedback for form fields by applying 'flashOutline' to highlight programmatically updated inputs and defining 'is-valid' styling (green border with checkmark) for valid form controls. --- FMS/Pages/Event/Edit.cshtml | 8 +- FMS/wwwroot/css/site.css | 97 ++++++++++++++---------- FMS/wwwroot/js/dateControlBorderFlash.js | 82 ++++++++++++++++++++ 3 files changed, 144 insertions(+), 43 deletions(-) create mode 100644 FMS/wwwroot/js/dateControlBorderFlash.js diff --git a/FMS/Pages/Event/Edit.cshtml b/FMS/Pages/Event/Edit.cshtml index 37d94a92..97b71682 100644 --- a/FMS/Pages/Event/Edit.cshtml +++ b/FMS/Pages/Event/Edit.cshtml @@ -116,14 +116,14 @@ - +
- +
@@ -132,12 +132,12 @@ - +
- diff --git a/FMS/wwwroot/css/site.css b/FMS/wwwroot/css/site.css index 418bf432..722c3cca 100644 --- a/FMS/wwwroot/css/site.css +++ b/FMS/wwwroot/css/site.css @@ -93,9 +93,9 @@ a:focus { text-decoration: underline; } -a:focus:not(.btn) { - text-decoration: none; -} + a:focus:not(.btn) { + text-decoration: none; + } .btn-primary:focus { color: #fff; @@ -159,7 +159,7 @@ a:focus:not(.btn) { .btn-outline-info { color: #fff; - background-color: #17a2b8; /*#5BAFD1;*/ /*#507033;*/ + background-color: #17a2b8; /*#5BAFD1;*/ /*#507033;*/ } .btn-outline-warning:focus { @@ -208,17 +208,17 @@ body { color: #008000; } -.footer a:hover, .footer a:focus { - color: #106717; -} + .footer a:hover, .footer a:focus { + color: #106717; + } .footer .bg-brand a { color: #ffff00; } -.footer .bg-brand a:hover, .footer .bg-brand a:focus { - color: #d9d900; -} + .footer .bg-brand a:hover, .footer .bg-brand a:focus { + color: #d9d900; + } /* Skip to content link -------------------------------------------------- */ @@ -234,15 +234,15 @@ body { z-index: 100; } -.skipnav:focus { - background: #fff; - left: 0; - outline: 0; - box-shadow: 0 0 3px #1B98CA, 0 0 7px #1B98CA; - position: absolute; - top: 0; - transition: all 0.2s ease-in-out; -} + .skipnav:focus { + background: #fff; + left: 0; + outline: 0; + box-shadow: 0 0 3px #1B98CA, 0 0 7px #1B98CA; + position: absolute; + top: 0; + transition: all 0.2s ease-in-out; + } /* Button with image -------------------------------------------------- */ @@ -250,9 +250,9 @@ body { line-height: 1; } -.btn-img img { - height: 35px; -} + .btn-img img { + height: 35px; + } .btn-img-bg { background: white; @@ -344,17 +344,17 @@ a[target]::after { border-left: 0.25rem solid var(--callout-border, var(--bs-gray-400)); } -.callout > :last-child { - margin-bottom: 0; -} + .callout > :last-child { + margin-bottom: 0; + } -.callout + .callout { - margin-top: -.25rem; -} + .callout + .callout { + margin-top: -.25rem; + } -.callout .highlight { - background-color: rgba(0, 0, 0, 0.05); -} + .callout .highlight { + background-color: rgba(0, 0, 0, 0.05); + } .callout-info { --callout-color: var(--bs-info-text-emphasis); @@ -384,9 +384,9 @@ a[target]::after { border: 1px solid; } - .banner-test-site::after { - content: "—Not for public distribution"; - } + .banner-test-site::after { + content: "—Not for public distribution"; + } } /* Chemical select box -------------------------------------------------- */ @@ -420,20 +420,39 @@ a[target]::after { --bs-accordion-border-color: #09601d; /* Border color */ } - /*1. Header Button when EXPANDED (Active)*/ +[data-bs-theme="dark"] .accordion { + --bs-accordion-bg: #c8f8d5; /* Body background */ + /*--bs-accordion-color: #0d5f06;*/ /* Body text color */ + --bs-accordion-btn-bg: #c8f8d5; /* Collapsed button background */ + --bs-accordion-btn-color: #212529; /* Collapsed button text */ + --bs-accordion-active-bg: #013901; /* Expanded button background */ + --bs-accordion-active-color: #c8f8d5; /* Expanded button text */ + --bs-accordion-border-color: #09601d; /* Border color */ +} + +/*1. Header Button when EXPANDED (Active)*/ .accordion-button:not(.collapsed) { - background-color: #013901; /*Custom active background */ - color: #c8f8d5; /*Custom active text color */ + background-color: #013901; /*Custom active background */ + color: #c8f8d5; /*Custom active text color */ } - /*2. Remove or change the blue focus halo outline*/ +/*2. Remove or change the blue focus halo outline*/ .accordion-button:focus { box-shadow: initial; border-color: #09601d; } - /*3. The inner body background */ +/*3. The inner body background */ .accordion-body { background-color: #013901; } +/* Valid form control styles for flashing boxes*/ +.form-control.is-valid, .was-validated .form-control:valid { + border-color: var(--bs-form-valid-border-color); + padding-right: calc(1.5em + .75rem); + background-image: url("data:image/svg+xml,"); + background-repeat: no-repeat; + background-position: right calc(.375em + .1875rem) center; + background-size: calc(.75em + .375rem) calc(.75em + .375rem); +} diff --git a/FMS/wwwroot/js/dateControlBorderFlash.js b/FMS/wwwroot/js/dateControlBorderFlash.js new file mode 100644 index 00000000..18316174 --- /dev/null +++ b/FMS/wwwroot/js/dateControlBorderFlash.js @@ -0,0 +1,82 @@ +document.addEventListener('DOMContentLoaded', () => { + const reportingPeriodType = document.getElementById('Item_ReportingPeriodType'); + const reportingPeriodStart = document.getElementById('Item_ReportingPeriodStart'); + const reportingPeriodEnd = document.getElementById('Item_ReportingPeriodEnd'); + + if (!reportingPeriodType || !reportingPeriodStart || !reportingPeriodEnd) return; + + reportingPeriodType.addEventListener('change', () => { + const today = new Date(); + const currentYear = today.getFullYear(); + const currentMonth = today.getMonth(); // 0-indexed + + let startDate, endDate; + + switch (reportingPeriodType.value) { + case '1': // Monthly + // Last month + startDate = new Date(currentYear, currentMonth - 1, 1); + endDate = new Date(currentYear, currentMonth, 0); + break; + + case '2': // First Quarter + startDate = new Date(currentYear - (currentMonth < 3 ? 1 : 0), 0, 1); + endDate = new Date(currentYear - (currentMonth < 3 ? 1 : 0), 3, 0); + break; + + case '3': // Second Quarter + startDate = new Date(currentYear - (currentMonth < 6 ? 1 : 0), 3, 1); + endDate = new Date(currentYear - (currentMonth < 6 ? 1 : 0), 6, 0); + break; + + case '4': // Third Quarter + startDate = new Date(currentYear - (currentMonth < 9 ? 1 : 0), 6, 1); + endDate = new Date(currentYear - (currentMonth < 9 ? 1 : 0), 9, 0); + break; + + case '5': // Fourth Quarter + startDate = new Date(currentYear - 1, 9, 1); + endDate = new Date(currentYear, 0, 0); + break; + + case '6': // First Semiannual + startDate = new Date(currentYear - (currentMonth < 6 ? 1 : 0), 0, 1); + endDate = new Date(currentYear - (currentMonth < 6 ? 1 : 0), 6, 0); + break; + + case '7': // Second Semiannual + startDate = new Date(currentYear - 1, 6, 1); + endDate = new Date(currentYear, 0, 0); + break; + + case '8': // Annual + startDate = new Date(currentYear - 1, 0, 1); + endDate = new Date(currentYear, 0, 0); + break; + + default: + return; + } + + if (startDate && endDate) { + reportingPeriodStart.value = FormatDate(startDate); + reportingPeriodEnd.value = FormatDate(endDate); + reportingPeriodStart.classList.remove('flashOutline'); + reportingPeriodEnd.classList.remove('flashOutline'); + // `offsetWidth` is only referenced here to trigger a layout reflow, so that adding back the class restarts the CSS animation. + // See: https://css-tricks.com/restart-css-animation/#aa-update-another-javascript-method-to-restart-a-css-animation + reportingPeriodStart.offsetWidth; //NOSONAR triggers reflow + reportingPeriodEnd.offsetWidth; //NOSONAR triggers reflow + reportingPeriodStart.classList.add('flashOutline'); + reportingPeriodEnd.classList.add('flashOutline'); + } + }); +}); + +function FormatDate(date) { + return [ + date.getFullYear(), + String(date.getMonth() + 1).padStart(2, '0'), + String(date.getDate()).padStart(2, '0'), + ].join('-'); +} From 5321e67a466927c20a7905c95d8090d5b68b8b5d Mon Sep 17 00:00:00 2001 From: Tom Karasch Date: Thu, 20 Aug 2026 12:22:09 -0400 Subject: [PATCH 2/3] 915: Add button to Facility Details page to view chemicals Introduces a new dedicated page for displaying and managing chemicals. This page includes client-side filtering and options to activate or deactivate chemicals. --- FMS/Pages/Facilities/Details.cshtml | 3 + FMS/Pages/Maintenance/Chemical/Detail.cshtml | 89 +++++++++++++++++++ .../Maintenance/Chemical/Detail.cshtml.cs | 68 ++++++++++++++ FMS/wwwroot/css/site.css | 6 +- 4 files changed, 163 insertions(+), 3 deletions(-) create mode 100644 FMS/Pages/Maintenance/Chemical/Detail.cshtml create mode 100644 FMS/Pages/Maintenance/Chemical/Detail.cshtml.cs diff --git a/FMS/Pages/Facilities/Details.cshtml b/FMS/Pages/Facilities/Details.cshtml index 96e0f9db..8d4cc2cf 100644 --- a/FMS/Pages/Facilities/Details.cshtml +++ b/FMS/Pages/Facilities/Details.cshtml @@ -1169,6 +1169,9 @@ } +
+ See Chemicals +
diff --git a/FMS/Pages/Maintenance/Chemical/Detail.cshtml b/FMS/Pages/Maintenance/Chemical/Detail.cshtml new file mode 100644 index 00000000..3ec81b10 --- /dev/null +++ b/FMS/Pages/Maintenance/Chemical/Detail.cshtml @@ -0,0 +1,89 @@ +@page +@using FMS.Pages.Maintenance +@model FMS.Pages.Maintenance.Chemical.DetailModel +@{ +} +@section Scripts { + + +} + +@{ + ViewData["Title"] = $"Site Maintenance: {MaintenanceOptions.Chemical}"; +} + +

+ @MaintenanceOptions.Chemical List Create New +

+ + +
+
+
+ +
+ + + (@(Model.Chemicals.Count > 0 ? Model.Chemicals.Count.ToString() : "none") found) + +
+
+
+
+
+
+
    + @for (var i = 0; i < Model.Chemicals.Count; i++) + { + var item = Model.Chemicals[i]; +
  • + @if (item.Active) + { +
    + +
    + } + else + { +
    + +
    + } + + @item.CasNo @item.ChemicalName + +
  • + } +
+
    +
    +
    \ No newline at end of file diff --git a/FMS/Pages/Maintenance/Chemical/Detail.cshtml.cs b/FMS/Pages/Maintenance/Chemical/Detail.cshtml.cs new file mode 100644 index 00000000..5d68776d --- /dev/null +++ b/FMS/Pages/Maintenance/Chemical/Detail.cshtml.cs @@ -0,0 +1,68 @@ +using FMS.Domain.Dto; +using FMS.Domain.Repositories; +using FMS.Platform.Extensions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; + +namespace FMS.Pages.Maintenance.Chemical +{ + public class DetailModel : PageModel + { + private readonly IChemicalRepository _repository; + public DetailModel(IChemicalRepository repository) => _repository = repository; + + public IReadOnlyList Chemicals { get; private set; } + public DisplayMessage DisplayMessage { get; private set; } + + public async Task OnGetAsync() + { + Chemicals = await _repository.GetChemicalListAsync(); + DisplayMessage = TempData?.GetDisplayMessage(); + return Page(); + } + + public async Task OnPostAsync(Guid? itemId) + { + if (itemId == null) + { + return BadRequest(); + } + + if (!ModelState.IsValid) + { + return Page(); + } + + var chemical = await _repository.GetChemicalByIdAsync(itemId.Value); + + if (chemical == null) + { + return NotFound(); + } + + try + { + await _repository.UpdateChemicalStatusAsync(itemId.Value, !chemical.Active); + } + catch (DbUpdateConcurrencyException) + { + if (!await _repository.ChemicalExistsAsync(itemId.Value)) + { + return NotFound(); + } + + throw; + } + + Chemicals = await _repository.GetChemicalListAsync(); + + TempData?.SetDisplayMessage(Context.Success, + chemical.Active + ? $"{MaintenanceOptions.Chemical} \"{chemical.ChemicalName}\" successfully removed from list." + : $"{MaintenanceOptions.Chemical} \"{chemical.ChemicalName}\" successfully restored."); + + return RedirectToPage("./Index"); + } + } +} diff --git a/FMS/wwwroot/css/site.css b/FMS/wwwroot/css/site.css index 722c3cca..b604fb9a 100644 --- a/FMS/wwwroot/css/site.css +++ b/FMS/wwwroot/css/site.css @@ -412,17 +412,17 @@ a[target]::after { /* Apply directly to your custom accordion or override globally */ .accordion { --bs-accordion-bg: #c8f8d5; /* Body background */ - /*--bs-accordion-color: #0d5f06;*/ /* Body text color */ + /*--bs-accordion-color: #013901;*/ /* Body text color */ --bs-accordion-btn-bg: #c8f8d5; /* Collapsed button background */ --bs-accordion-btn-color: #212529; /* Collapsed button text */ --bs-accordion-active-bg: #013901; /* Expanded button background */ - --bs-accordion-active-color: #c8f8d5; /* Expanded button text */ + --bs-accordion-active-color: #fff; /* Expanded button text */ --bs-accordion-border-color: #09601d; /* Border color */ } [data-bs-theme="dark"] .accordion { --bs-accordion-bg: #c8f8d5; /* Body background */ - /*--bs-accordion-color: #0d5f06;*/ /* Body text color */ + --bs-accordion-color: #c8f8d5; /* Body text color */ --bs-accordion-btn-bg: #c8f8d5; /* Collapsed button background */ --bs-accordion-btn-color: #212529; /* Collapsed button text */ --bs-accordion-active-bg: #013901; /* Expanded button background */ From 7ad07303a84f4bac8d430af923ceb4fd76ea1686 Mon Sep 17 00:00:00 2001 From: Tom Karasch Date: Fri, 21 Aug 2026 20:06:07 -0400 Subject: [PATCH 3/3] 915: Enhance chemical list details and table header stickiness * The Chemical List (`Maintenance/Chemical/Detail`) page is enhanced to allow users to select a chemical, displaying its comprehensive details (e.g., CAS No, chemical name, tox value) in a dedicated, sticky header section for quick reference. The filter bar on this page is also made sticky. * All data tables on the Dashboard (`Index.cshtml`) now feature sticky headers, improving readability and navigation when scrolling through long lists of events and facilities. * The "See Chemical List" button on the Facility Details page (`Facilities/Details.cshtml`) has been repositioned for improved visibility and user flow. --- FMS/Pages/Dashboard/Index.cshtml | 36 +++--- FMS/Pages/Facilities/Details.cshtml | 4 +- FMS/Pages/Maintenance/Chemical/Detail.cshtml | 110 +++++++++++++++--- .../Maintenance/Chemical/Detail.cshtml.cs | 3 + 4 files changed, 114 insertions(+), 39 deletions(-) diff --git a/FMS/Pages/Dashboard/Index.cshtml b/FMS/Pages/Dashboard/Index.cshtml index 332687d0..28e0e68c 100644 --- a/FMS/Pages/Dashboard/Index.cshtml +++ b/FMS/Pages/Dashboard/Index.cshtml @@ -42,7 +42,7 @@ @if (Model.UserEvents.Where(f => f.CompletionDate == null && f.StartDate == null).Count() > 0) { - + @@ -116,7 +116,7 @@ @if (Model.UserEvents.Where(f => f.CompletionDate == null && f.StartDate != null).Count() > 0) {
    Facility Number Facility Name
    - + @@ -190,7 +190,7 @@ @if (Model.UserEvents.Where(f => f.CompletionDate != null).Count() > 0) {
    Facility Number Facility Name
    - + @@ -312,7 +312,7 @@ @if (Model.UnitEvents.Where(f => f.CompletionDate == null && f.StartDate == null && f.ComplianceOfficer?.Name == co).Count() > 0) {
    Facility Number Facility Name
    - + @@ -386,7 +386,7 @@ @if (Model.UnitEvents.Where(f => f.CompletionDate == null && f.StartDate != null && f.ComplianceOfficer?.Name == co).Count() > 0) {
    Facility Number Facility Name
    - + @@ -460,7 +460,7 @@ @if (Model.UnitEvents.Where(e => e.CompletionDate != null && e.ComplianceOfficer?.Name == co).Count() > 0) {
    Facility Number Facility Name
    - + @@ -609,7 +609,7 @@
    Facility Number Facility Name
    - + @@ -692,7 +692,7 @@
    Facility Number Facility Name
    - + @@ -775,7 +775,7 @@
    Facility Number Facility Name
    - + @@ -905,7 +905,7 @@ @if (Model.UserFacilities.Where(f => f.FacilityStatus.Name.Equals("Active")).Count() > 0) {
    Facility Number Facility Name
    - + @@ -963,7 +963,7 @@ @if (Model.UserFacilities.Where(f => f.FacilityStatus.Name.Equals("DELISTED")).Count() > 0) {
    Number Name
    - + @@ -1021,7 +1021,7 @@ @if (Model.UserFacilities.Where(f => !f.FacilityStatus.Name.Equals("DELISTED") && !f.FacilityStatus.Name.Equals("Active")).Count() > 0) {
    Number Name
    - + @@ -1127,7 +1127,7 @@
    Number Name
    - + @@ -1185,7 +1185,7 @@
    Number Name
    - + @@ -1243,7 +1243,7 @@
    Number Name
    - + @@ -1354,7 +1354,7 @@
    Number Name
    - + @@ -1412,7 +1412,7 @@
    Number Name
    - + @@ -1471,7 +1471,7 @@
    Number Name
    - + diff --git a/FMS/Pages/Facilities/Details.cshtml b/FMS/Pages/Facilities/Details.cshtml index 8d4cc2cf..fd4d6310 100644 --- a/FMS/Pages/Facilities/Details.cshtml +++ b/FMS/Pages/Facilities/Details.cshtml @@ -1128,6 +1128,7 @@ { Add Substance } + See Chemical List
    @@ -1169,9 +1170,6 @@ }
    Number Name
    - diff --git a/FMS/Pages/Maintenance/Chemical/Detail.cshtml b/FMS/Pages/Maintenance/Chemical/Detail.cshtml index 3ec81b10..98ebc107 100644 --- a/FMS/Pages/Maintenance/Chemical/Detail.cshtml +++ b/FMS/Pages/Maintenance/Chemical/Detail.cshtml @@ -32,6 +32,46 @@ filteredList.appendChild(fragment); countOutput.textContent = count > 0 ? ` (${count} found)` : ' (none found)'; }); + + originalList.addEventListener('mouseup', (event) => { + if (event.target.tagName === 'A') { + const selectedItem = event.target.closest('a'); + const casNo = selectedItem.getAttribute('data-casno'); + const chemicalName = selectedItem.getAttribute('data-chemicalname'); + const commonName = selectedItem.getAttribute('data-commonname'); + const toxValue = selectedItem.getAttribute('data-toxvalue'); + const mcls = selectedItem.getAttribute('data-mcls'); + const finalRc = selectedItem.getAttribute('data-finalrc'); + const rq = selectedItem.getAttribute('data-rq'); + document.getElementById('casno').value = casNo; + document.getElementById('ChemicalName').value = chemicalName; + document.getElementById('CommonName').value = commonName; + document.getElementById('ToxValue').value = toxValue; + document.getElementById('MCLs').value = mcls; + document.getElementById('FinalRc').value = finalRc; + document.getElementById('RQ').value = rq; + } + }); + + filteredList.addEventListener('mouseup', (event) => { + if (event.target.tagName === 'A') { + const selectedItem = event.target.closest('a'); + const casNo = selectedItem.getAttribute('data-casno'); + const chemicalName = selectedItem.getAttribute('data-chemicalname'); + const commonName = selectedItem.getAttribute('data-commonname'); + const toxValue = selectedItem.getAttribute('data-toxvalue'); + const mcls = selectedItem.getAttribute('data-mcls'); + const finalRc = selectedItem.getAttribute('data-finalrc'); + const rq = selectedItem.getAttribute('data-rq'); + document.getElementById('casno').value = casNo; + document.getElementById('ChemicalName').value = chemicalName; + document.getElementById('CommonName').value = commonName; + document.getElementById('ToxValue').value = toxValue; + document.getElementById('MCLs').value = mcls; + document.getElementById('FinalRc').value = finalRc; + document.getElementById('RQ').value = rq; + } + }); }); } @@ -41,14 +81,14 @@ }

    - @MaintenanceOptions.Chemical List Create New + @MaintenanceOptions.Chemical

    -
    +
    - +
    @@ -56,6 +96,46 @@
    +
    +
    +
    + + + +
    +
    + + + +
    +
    + + + +
    +
    +
    +
    + + + +
    +
    + + + +
    +
    + + + +
    +
    + + + +
    +
    @@ -64,21 +144,15 @@ @for (var i = 0; i < Model.Chemicals.Count; i++) { var item = Model.Chemicals[i]; -
  • - @if (item.Active) - { -
    - -
    - } - else - { -
    - -
    - } - +
  • + @item.CasNo @item.ChemicalName
  • diff --git a/FMS/Pages/Maintenance/Chemical/Detail.cshtml.cs b/FMS/Pages/Maintenance/Chemical/Detail.cshtml.cs index 5d68776d..22eece2b 100644 --- a/FMS/Pages/Maintenance/Chemical/Detail.cshtml.cs +++ b/FMS/Pages/Maintenance/Chemical/Detail.cshtml.cs @@ -13,6 +13,9 @@ public class DetailModel : PageModel public DetailModel(IChemicalRepository repository) => _repository = repository; public IReadOnlyList Chemicals { get; private set; } + + public ChemicalSummaryDto Chemical { get; } + public DisplayMessage DisplayMessage { get; private set; } public async Task OnGetAsync()