From 36d4ae00ddf78a2fab28ec523a1a11a7f891301f Mon Sep 17 00:00:00 2001 From: Blake Ridgway Date: Sat, 21 Jun 2025 16:30:59 -0500 Subject: [PATCH] feat: Enhance UI/UX across multiple views with consistent styling and improved layout --- Controllers/LawnCareEventsContoller.cs | 51 ++++--- ..._AddNotApplicableMowingPattern.Designer.cs | 72 +++++++++ ...619022142_AddNotApplicableMowingPattern.cs | 22 +++ ...20250621212709_AddEventDetails.Designer.cs | 86 +++++++++++ Migrations/20250621212709_AddEventDetails.cs | 60 ++++++++ .../ApplicationDbContextModelSnapshot.cs | 14 ++ Models/LawnCareEvent.cs | 27 +++- Views/LawnCareEvents/Calendar.cshtml | 137 ++++++++++++++++++ Views/LawnCareEvents/Delete.cshtml | 8 +- Views/LawnCareEvents/Details.cshtml | 1 + Views/LawnCareEvents/Edit.cshtml | 46 +++--- Views/LawnCareEvents/Index.cshtml | 23 +-- Views/LawnCareTip/Index.cshtml | 11 +- Views/Shared/_Layout.cshtml | 3 +- wwwroot/css/calendar.css | 95 ++++++++++++ wwwroot/css/site.css | 13 ++ 16 files changed, 609 insertions(+), 60 deletions(-) create mode 100644 Migrations/20250619022142_AddNotApplicableMowingPattern.Designer.cs create mode 100644 Migrations/20250619022142_AddNotApplicableMowingPattern.cs create mode 100644 Migrations/20250621212709_AddEventDetails.Designer.cs create mode 100644 Migrations/20250621212709_AddEventDetails.cs create mode 100644 Views/LawnCareEvents/Calendar.cshtml create mode 100644 wwwroot/css/calendar.css diff --git a/Controllers/LawnCareEventsContoller.cs b/Controllers/LawnCareEventsContoller.cs index de6e138..ab9fd2d 100644 --- a/Controllers/LawnCareEventsContoller.cs +++ b/Controllers/LawnCareEventsContoller.cs @@ -14,19 +14,15 @@ namespace turf_tasker.Controllers _context = context; } - // GET: LawnCareEvents public async Task Index(string sortOrder, string searchString) { - // Use ViewData to pass sort order info to the view ViewData["DateSortParm"] = String.IsNullOrEmpty(sortOrder) ? "date_desc" : ""; ViewData["TypeSortParm"] = sortOrder == "Type" ? "type_desc" : "Type"; ViewData["CurrentFilter"] = searchString; - // Start with a base query that can be modified var events = from e in _context.LawnCareEvents select e; - // Apply the filter if a search string is provided if (!String.IsNullOrEmpty(searchString)) { events = events.Where(e => @@ -35,7 +31,6 @@ namespace turf_tasker.Controllers ); } - // Apply the sorting based on the sortOrder parameter switch (sortOrder) { case "date_desc": @@ -51,12 +46,9 @@ namespace turf_tasker.Controllers events = events.OrderBy(e => e.EventDate); break; } - - // Execute the query and return the view with the filtered/sorted list return View(await events.AsNoTracking().ToListAsync()); } - // GET: LawnCareEvents/Details/5 public async Task Details(int? id) { if (id == null) @@ -74,15 +66,13 @@ namespace turf_tasker.Controllers return View(lawnCareEvent); } - // GET: LawnCareEvents/Create - // MODIFIED: We now create a model with a default date. + public IActionResult Create() { var model = new LawnCareEvent { EventDate = DateTime.Now }; return View(model); } - // POST: LawnCareEvents/Create [HttpPost] [ValidateAntiForgeryToken] public async Task Create([Bind("Id,EventType,EventDate,MowingPattern,Notes")] LawnCareEvent lawnCareEvent) @@ -95,8 +85,7 @@ namespace turf_tasker.Controllers } return View(lawnCareEvent); } - - // GET: LawnCareEvents/Edit/5 + public async Task Edit(int? id) { if (id == null) @@ -112,7 +101,6 @@ namespace turf_tasker.Controllers return View(lawnCareEvent); } - // POST: LawnCareEvents/Edit/5 [HttpPost] [ValidateAntiForgeryToken] public async Task Edit(int id, [Bind("Id,EventType,EventDate,MowingPattern,Notes")] LawnCareEvent lawnCareEvent) @@ -131,7 +119,7 @@ namespace turf_tasker.Controllers } catch (DbUpdateConcurrencyException) { - if (!LawnCareEventExists(lawnCareEvent.Id)) + if (!LawnCareEventExists(lawnCareEvent.Id)) // This uses the private method below { return NotFound(); } @@ -145,7 +133,6 @@ namespace turf_tasker.Controllers return View(lawnCareEvent); } - // GET: LawnCareEvents/Delete/5 public async Task Delete(int? id) { if (id == null) @@ -163,7 +150,6 @@ namespace turf_tasker.Controllers return View(lawnCareEvent); } - // POST: LawnCareEvents/Delete/5 [HttpPost, ActionName("Delete")] [ValidateAntiForgeryToken] public async Task DeleteConfirmed(int id) @@ -182,5 +168,36 @@ namespace turf_tasker.Controllers { return _context.LawnCareEvents.Any(e => e.Id == id); } + + public IActionResult Calendar() + { + return View(); + } + + [HttpGet] + public async Task GetEventsJson() + { + var events = await _context.LawnCareEvents.AsNoTracking().ToListAsync(); + + var calendarEvents = events.Select(e => new + { + id = e.Id, + title = $"{e.EventType} {((e.EventType == LawnCareEventType.Mowing && e.MowingPattern.HasValue) ? $"({e.MowingPattern})" : "")}", + start = e.EventDate.ToString("yyyy-MM-dd"), + allDay = true, + url = $"/LawnCareEvents/Details/{e.Id}", + color = e.EventType switch + { + LawnCareEventType.Mowing => "#28a745", + LawnCareEventType.Watering => "#007bff", + LawnCareEventType.Fertilizing => "#ffc107", + LawnCareEventType.Aeration => "#6f42c1", + LawnCareEventType.WeedControl => "#dc3545", + _ => "#6c757d" + } + }).ToList(); + + return Json(calendarEvents); + } } } \ No newline at end of file diff --git a/Migrations/20250619022142_AddNotApplicableMowingPattern.Designer.cs b/Migrations/20250619022142_AddNotApplicableMowingPattern.Designer.cs new file mode 100644 index 0000000..1952d3d --- /dev/null +++ b/Migrations/20250619022142_AddNotApplicableMowingPattern.Designer.cs @@ -0,0 +1,72 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using turf_tasker.Data; + +#nullable disable + +namespace turf_tasker.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250619022142_AddNotApplicableMowingPattern")] + partial class AddNotApplicableMowingPattern + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.6"); + + modelBuilder.Entity("turf_tasker.Models.LawnCareEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EventDate") + .HasColumnType("TEXT"); + + b.Property("EventType") + .HasColumnType("INTEGER"); + + b.Property("MowingPattern") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("LawnCareEvents"); + }); + + modelBuilder.Entity("turf_tasker.Models.LawnCareTip", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Category") + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("LawnCareTips"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20250619022142_AddNotApplicableMowingPattern.cs b/Migrations/20250619022142_AddNotApplicableMowingPattern.cs new file mode 100644 index 0000000..a94da34 --- /dev/null +++ b/Migrations/20250619022142_AddNotApplicableMowingPattern.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace turf_tasker.Migrations +{ + /// + public partial class AddNotApplicableMowingPattern : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/Migrations/20250621212709_AddEventDetails.Designer.cs b/Migrations/20250621212709_AddEventDetails.Designer.cs new file mode 100644 index 0000000..0eb8d9c --- /dev/null +++ b/Migrations/20250621212709_AddEventDetails.Designer.cs @@ -0,0 +1,86 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using turf_tasker.Data; + +#nullable disable + +namespace turf_tasker.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250621212709_AddEventDetails")] + partial class AddEventDetails + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.6"); + + modelBuilder.Entity("turf_tasker.Models.LawnCareEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppliedAmount") + .HasColumnType("TEXT"); + + b.Property("EventDate") + .HasColumnType("TEXT"); + + b.Property("EventType") + .HasColumnType("INTEGER"); + + b.Property("MowerHeightInches") + .HasColumnType("TEXT"); + + b.Property("MowingPattern") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("ProblemObserved") + .HasMaxLength(250) + .HasColumnType("TEXT"); + + b.Property("ProductUsed") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("LawnCareEvents"); + }); + + modelBuilder.Entity("turf_tasker.Models.LawnCareTip", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Category") + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("LawnCareTips"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20250621212709_AddEventDetails.cs b/Migrations/20250621212709_AddEventDetails.cs new file mode 100644 index 0000000..97662ed --- /dev/null +++ b/Migrations/20250621212709_AddEventDetails.cs @@ -0,0 +1,60 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace turf_tasker.Migrations +{ + /// + public partial class AddEventDetails : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AppliedAmount", + table: "LawnCareEvents", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "MowerHeightInches", + table: "LawnCareEvents", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "ProblemObserved", + table: "LawnCareEvents", + type: "TEXT", + maxLength: 250, + nullable: true); + + migrationBuilder.AddColumn( + name: "ProductUsed", + table: "LawnCareEvents", + type: "TEXT", + maxLength: 200, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "AppliedAmount", + table: "LawnCareEvents"); + + migrationBuilder.DropColumn( + name: "MowerHeightInches", + table: "LawnCareEvents"); + + migrationBuilder.DropColumn( + name: "ProblemObserved", + table: "LawnCareEvents"); + + migrationBuilder.DropColumn( + name: "ProductUsed", + table: "LawnCareEvents"); + } + } +} diff --git a/Migrations/ApplicationDbContextModelSnapshot.cs b/Migrations/ApplicationDbContextModelSnapshot.cs index f6d0c56..320c9f9 100644 --- a/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Migrations/ApplicationDbContextModelSnapshot.cs @@ -23,12 +23,18 @@ namespace turf_tasker.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("AppliedAmount") + .HasColumnType("TEXT"); + b.Property("EventDate") .HasColumnType("TEXT"); b.Property("EventType") .HasColumnType("INTEGER"); + b.Property("MowerHeightInches") + .HasColumnType("TEXT"); + b.Property("MowingPattern") .HasColumnType("INTEGER"); @@ -36,6 +42,14 @@ namespace turf_tasker.Migrations .HasMaxLength(500) .HasColumnType("TEXT"); + b.Property("ProblemObserved") + .HasMaxLength(250) + .HasColumnType("TEXT"); + + b.Property("ProductUsed") + .HasMaxLength(200) + .HasColumnType("TEXT"); + b.HasKey("Id"); b.ToTable("LawnCareEvents"); diff --git a/Models/LawnCareEvent.cs b/Models/LawnCareEvent.cs index 604d8de..4b8d9c3 100644 --- a/Models/LawnCareEvent.cs +++ b/Models/LawnCareEvent.cs @@ -9,11 +9,17 @@ public enum LawnCareEventType Fertilizing, Aeration, WeedControl, + PestControl, + DiseaseControl, + Topdressing, + Overseeding, Other } public enum MowingPattern { + [Display(Name = "N/A")] + NotApplicable, Vertical, Horizontal, Diagonal, @@ -29,17 +35,30 @@ public class LawnCareEvent [Required] [Display(Name = "Activity Type")] public LawnCareEventType EventType { get; set; } - - // --- THIS IS THE LINE TO CHECK --- - // Make sure this property exists and is spelled correctly. + [Required] [DataType(DataType.Date)] [Display(Name = "Date")] public DateTime EventDate { get; set; } - // --------------------------------- [Display(Name = "Mowing Pattern")] public MowingPattern? MowingPattern { get; set; } + + [Display(Name = "Product Used")] + [StringLength(200)] + public string? ProductUsed { get; set; } + + [Display(Name = "Applied Amount")] + [Range(0.01, 1000.0, ErrorMessage = "Amount must be positive.")] // Example range + public decimal? AppliedAmount { get; set; } + + [Display(Name = "Mower Height (inches)")] + [Range(0.5, 6.0, ErrorMessage = "Height must be between 0.5 and 6 inches.")] + public decimal? MowerHeightInches { get; set; } + + [Display(Name = "Problem Observed")] + [StringLength(250)] + public string? ProblemObserved { get; set; } [StringLength(500)] public string? Notes { get; set; } diff --git a/Views/LawnCareEvents/Calendar.cshtml b/Views/LawnCareEvents/Calendar.cshtml new file mode 100644 index 0000000..0db49ff --- /dev/null +++ b/Views/LawnCareEvents/Calendar.cshtml @@ -0,0 +1,137 @@ +@{ + ViewData["Title"] = "Lawn Care Calendar"; +} + +
+

@ViewData["Title"]

+ + Create New Event + +
+ +
+
+ +

+ +
+
+
Sun
+
Mon
+
Tue
+
Wed
+
Thu
+
Fri
+
Sat
+
+
+ +
+
+ +@section Scripts { + + +} + +@section Styles { + +} \ No newline at end of file diff --git a/Views/LawnCareEvents/Delete.cshtml b/Views/LawnCareEvents/Delete.cshtml index 101b2f0..42b8768 100644 --- a/Views/LawnCareEvents/Delete.cshtml +++ b/Views/LawnCareEvents/Delete.cshtml @@ -39,7 +39,11 @@
- | - Back to List + +
+ + \ No newline at end of file diff --git a/Views/LawnCareEvents/Details.cshtml b/Views/LawnCareEvents/Details.cshtml index d76c146..08013e6 100644 --- a/Views/LawnCareEvents/Details.cshtml +++ b/Views/LawnCareEvents/Details.cshtml @@ -43,6 +43,7 @@
+ Delete
diff --git a/Views/LawnCareEvents/Edit.cshtml b/Views/LawnCareEvents/Edit.cshtml index 47642ec..0be630c 100644 --- a/Views/LawnCareEvents/Edit.cshtml +++ b/Views/LawnCareEvents/Edit.cshtml @@ -1,72 +1,74 @@ @model turf_tasker.Models.LawnCareEvent @{ - ViewData["Title"] = "Edit"; +ViewData["Title"] = "Edit"; }

Edit

-

LawnCareEvent

+

Lawn Care Event


-
+
-
+
-
+
-
+
+ + Delete
-
+ @section Scripts { - @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} +@{await Html.RenderPartialAsync("_ValidationScriptsPartial");} - + }); + } \ No newline at end of file diff --git a/Views/LawnCareEvents/Index.cshtml b/Views/LawnCareEvents/Index.cshtml index 55439ca..5c79329 100644 --- a/Views/LawnCareEvents/Index.cshtml +++ b/Views/LawnCareEvents/Index.cshtml @@ -4,11 +4,14 @@ ViewData["Title"] = "Lawn Care Log"; } -

@ViewData["Title"]

+
+

@ViewData["Title"]

-

- Create New Event -

+

+ Create New Event + View Calendar +

+
@@ -16,8 +19,8 @@

Find by type or note: - | - Back to Full List + + Reset Filter

@@ -63,9 +66,11 @@ @Html.DisplayFor(modelItem => item.Notes) - Edit | - Details | - Delete +
+ Edit + Details + Delete +
} diff --git a/Views/LawnCareTip/Index.cshtml b/Views/LawnCareTip/Index.cshtml index 694efe3..edd49e9 100644 --- a/Views/LawnCareTip/Index.cshtml +++ b/Views/LawnCareTip/Index.cshtml @@ -7,7 +7,7 @@

Index

- Create New + Create New Tip

@@ -36,10 +36,11 @@ - } diff --git a/Views/Shared/_Layout.cshtml b/Views/Shared/_Layout.cshtml index f73bc46..c27a958 100644 --- a/Views/Shared/_Layout.cshtml +++ b/Views/Shared/_Layout.cshtml @@ -5,6 +5,8 @@ @ViewData["Title"] - MowLog + @await RenderSectionAsync("Scripts", required: false) + @await RenderSectionAsync("Styles", required: false) @@ -46,6 +48,5 @@ - @await RenderSectionAsync("Scripts", required: false) \ No newline at end of file diff --git a/wwwroot/css/calendar.css b/wwwroot/css/calendar.css new file mode 100644 index 0000000..88e010f --- /dev/null +++ b/wwwroot/css/calendar.css @@ -0,0 +1,95 @@ +.calendar-container { + max-width: 900px; + margin: 20px auto; + font-family: Arial, sans-serif; + border: 1px solid #ddd; + border-radius: 5px; + overflow: hidden; + background-color: #fff; + box-shadow: 0 2px 5px rgba(0,0,0,0.1); +} + +.calendar-header { + display: flex; + justify-content: space-between; + align-items: center; + background-color: #f8f9fa; + padding: 15px; + border-bottom: 1px solid #ddd; +} + +.calendar-header h2 { + margin: 0; + font-size: 1.5em; + color: #333; +} + +.calendar-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 1px; + background-color: #ddd; + border-bottom: 1px solid #ddd; +} + +.day-name { + background-color: #e9ecef; + padding: 10px 5px; + text-align: center; + font-weight: bold; + color: #555; + font-size: 0.9em; +} + +.calendar-day { + background-color: #fff; + padding: 10px 5px; + min-height: 100px; + text-align: right; + position: relative; + cursor: pointer; + transition: background-color 0.2s ease; + box-sizing: border-box; + border: 1px solid #eee; +} + +.calendar-day:hover { + background-color: #f0f0f0; +} + +.calendar-day.other-month { + background-color: #f9f9f9; + color: #ccc; +} + +.calendar-day .day-number { + font-weight: bold; + font-size: 1.2em; + color: #333; + position: absolute; + top: 5px; + right: 5px; +} + +.calendar-day .event-indicator { + background-color: #28a745; + color: white; + font-size: 0.75em; + padding: 2px 5px; + border-radius: 3px; + margin-top: 25px; + display: inline-block; + margin-left: 5px; + margin-right: 5px; +} + +.event-indicator.mowing { background-color: #28a745; } +.event-indicator.watering { background-color: #007bff; } +.event-indicator.fertilizing { background-color: #ffc107; } +.event-indicator.aeration { background-color: #6f42c1; } +.event-indicator.weedcontrol { background-color: #dc3545; } +.event-indicator.other { background-color: #6c757d; } + +.card.h-100 { + height: 100% !important; +} \ No newline at end of file diff --git a/wwwroot/css/site.css b/wwwroot/css/site.css index efacf73..c019ffe 100644 --- a/wwwroot/css/site.css +++ b/wwwroot/css/site.css @@ -33,4 +33,17 @@ body { .navbar-custom .nav-link:hover { color: #d4d4d4; /* A light gray for hover effect */ +} + +/* For tables, to control column widths */ +.table .col-content { + max-width: 400px; /* Adjust as needed */ + word-wrap: break-word; /* Break long words */ + white-space: normal; /* Allow text to wrap normally */ +} + +.table .col-actions { + width: 150px; /* Fixed width for action buttons, adjust as needed */ + min-width: 120px; /* Ensure minimum space */ + white-space: nowrap; /* Keep buttons on one line, prevent wrapping */ } \ No newline at end of file
@Html.DisplayFor(modelItem => item.Content) - Edit | - Details | - Delete + +
+ Edit + Delete +