diff --git a/Controllers/HomeController.cs b/Controllers/HomeController.cs index 4adc1a3..5fc11b7 100644 --- a/Controllers/HomeController.cs +++ b/Controllers/HomeController.cs @@ -10,22 +10,18 @@ public class HomeController : Controller { private readonly ApplicationDbContext _context; - // Step 2.1: Inject the database context public HomeController(ApplicationDbContext context) { _context = context; } - // Step 2.2: Make the Index action async public async Task Index() { - // Step 2.3: Query for the last mow event to get its date and pattern var lastMowEvent = await _context.LawnCareEvents .Where(e => e.EventType == LawnCareEventType.Mowing) .OrderByDescending(e => e.EventDate) .FirstOrDefaultAsync(); - // Step 2.4: Query for the last water and fertilize dates var lastWaterDate = await _context.LawnCareEvents .Where(e => e.EventType == LawnCareEventType.Watering) .OrderByDescending(e => e.EventDate) @@ -38,6 +34,12 @@ public class HomeController : Controller .Select(e => (DateTime?)e.EventDate) .FirstOrDefaultAsync(); + var lastAerationDate = await _context.LawnCareEvents + .Where(e => e.EventType == LawnCareEventType.Aeration) + .OrderByDescending(e => e.EventDate) + .Select(e => (DateTime?)e.EventDate) + .FirstOrDefaultAsync(); + // Determine the next mowing pattern MowingPattern? nextPattern = MowingPattern.Vertical; // Default if (lastMowEvent?.MowingPattern != null) @@ -48,16 +50,15 @@ public class HomeController : Controller nextPattern = (MowingPattern)nextPatternValue; } - // Step 2.5: Create the ViewModel and populate it var viewModel = new DashboardViewModel { LastMowDate = lastMowEvent?.EventDate, LastWaterDate = lastWaterDate, LastFertilizeDate = lastFertilizeDate, + LastAerationDate = lastAerationDate, NextMowingPattern = nextPattern }; - - // Step 2.6: Pass the populated ViewModel to the view + return View(viewModel); } diff --git a/Controllers/LawnCareTipController.cs b/Controllers/LawnCareTipController.cs new file mode 100644 index 0000000..f50ba94 --- /dev/null +++ b/Controllers/LawnCareTipController.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.EntityFrameworkCore; +using turf_tasker.Data; +using turf_tasker.Models; + +namespace turf_tasker.Controllers +{ + public class LawnCareTipController : Controller + { + private readonly ApplicationDbContext _context; + + public LawnCareTipController(ApplicationDbContext context) + { + _context = context; + } + + // GET: LawnCareTip + public async Task Index() + { + return View(await _context.LawnCareTips.ToListAsync()); + } + + // GET: LawnCareTip/Details/5 + public async Task Details(int? id) + { + if (id == null) + { + return NotFound(); + } + + var lawnCareTip = await _context.LawnCareTips + .FirstOrDefaultAsync(m => m.Id == id); + if (lawnCareTip == null) + { + return NotFound(); + } + + return View(lawnCareTip); + } + + // GET: LawnCareTip/Create + public IActionResult Create() + { + return View(); + } + + // POST: LawnCareTip/Create + // To protect from overposting attacks, enable the specific properties you want to bind to. + // For more details, see http://go.microsoft.com/fwlink/?LinkId=317598. + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Create([Bind("Id,Title,Category,Content")] LawnCareTip lawnCareTip) + { + if (ModelState.IsValid) + { + _context.Add(lawnCareTip); + await _context.SaveChangesAsync(); + return RedirectToAction(nameof(Index)); + } + return View(lawnCareTip); + } + + // GET: LawnCareTip/Edit/5 + public async Task Edit(int? id) + { + if (id == null) + { + return NotFound(); + } + + var lawnCareTip = await _context.LawnCareTips.FindAsync(id); + if (lawnCareTip == null) + { + return NotFound(); + } + return View(lawnCareTip); + } + + // POST: LawnCareTip/Edit/5 + // To protect from overposting attacks, enable the specific properties you want to bind to. + // For more details, see http://go.microsoft.com/fwlink/?LinkId=317598. + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Edit(int id, [Bind("Id,Title,Category,Content")] LawnCareTip lawnCareTip) + { + if (id != lawnCareTip.Id) + { + return NotFound(); + } + + if (ModelState.IsValid) + { + try + { + _context.Update(lawnCareTip); + await _context.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + if (!LawnCareTipExists(lawnCareTip.Id)) + { + return NotFound(); + } + else + { + throw; + } + } + return RedirectToAction(nameof(Index)); + } + return View(lawnCareTip); + } + + // GET: LawnCareTip/Delete/5 + public async Task Delete(int? id) + { + if (id == null) + { + return NotFound(); + } + + var lawnCareTip = await _context.LawnCareTips + .FirstOrDefaultAsync(m => m.Id == id); + if (lawnCareTip == null) + { + return NotFound(); + } + + return View(lawnCareTip); + } + + // POST: LawnCareTip/Delete/5 + [HttpPost, ActionName("Delete")] + [ValidateAntiForgeryToken] + public async Task DeleteConfirmed(int id) + { + var lawnCareTip = await _context.LawnCareTips.FindAsync(id); + if (lawnCareTip != null) + { + _context.LawnCareTips.Remove(lawnCareTip); + } + + await _context.SaveChangesAsync(); + return RedirectToAction(nameof(Index)); + } + + private bool LawnCareTipExists(int id) + { + return _context.LawnCareTips.Any(e => e.Id == id); + } + } +} diff --git a/Data/ApplicationDbContext.cs b/Data/ApplicationDbContext.cs index 760d99c..e919f1a 100644 --- a/Data/ApplicationDbContext.cs +++ b/Data/ApplicationDbContext.cs @@ -9,4 +9,6 @@ public class ApplicationDbContext : DbContext : base(options) { } public DbSet LawnCareEvents { get; set; } + + public DbSet LawnCareTips { get; set; } } \ No newline at end of file diff --git a/Migrations/20250617225541_AddLawnCareTips.Designer.cs b/Migrations/20250617225541_AddLawnCareTips.Designer.cs new file mode 100644 index 0000000..76b5f14 --- /dev/null +++ b/Migrations/20250617225541_AddLawnCareTips.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("20250617225541_AddLawnCareTips")] + partial class AddLawnCareTips + { + /// + 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/20250617225541_AddLawnCareTips.cs b/Migrations/20250617225541_AddLawnCareTips.cs new file mode 100644 index 0000000..3a1003a --- /dev/null +++ b/Migrations/20250617225541_AddLawnCareTips.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace turf_tasker.Migrations +{ + /// + public partial class AddLawnCareTips : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "LawnCareTips", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Title = table.Column(type: "TEXT", maxLength: 100, nullable: false), + Category = table.Column(type: "INTEGER", nullable: false), + Content = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_LawnCareTips", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "LawnCareTips"); + } + } +} diff --git a/Migrations/20250617225855_SeedBermudaTips.Designer.cs b/Migrations/20250617225855_SeedBermudaTips.Designer.cs new file mode 100644 index 0000000..f958981 --- /dev/null +++ b/Migrations/20250617225855_SeedBermudaTips.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("20250617225855_SeedBermudaTips")] + partial class SeedBermudaTips + { + /// + 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/20250617225855_SeedBermudaTips.cs b/Migrations/20250617225855_SeedBermudaTips.cs new file mode 100644 index 0000000..22788b2 --- /dev/null +++ b/Migrations/20250617225855_SeedBermudaTips.cs @@ -0,0 +1,70 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace turf_tasker.Migrations +{ +public partial class SeedBermudaTips : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + // Seed data for LawnCareTips + migrationBuilder.InsertData( + table: "LawnCareTips", + columns: new[] { "Id", "Title", "Category", "Content" }, + values: new object[,] + { + { 1, "Bermuda Mowing Height", 0, "For Bermuda grass in Oklahoma, mow at a height of 1-2 inches for optimal density and health during peak season." }, + { 2, "Bermuda Mowing Frequency", 0, "During peak growing season (late spring/summer), mow Bermuda grass every 3-5 days. Frequent mowing prevents scalping." }, + { 3, "Bermuda Watering - Deep & Infrequent", 1, "Water Bermuda grass deeply (1-1.5 inches) once or twice a week, rather than frequent, shallow watering. Water in the early morning." }, + { 4, "Bermuda Spring Fertilization", 2, "Apply a nitrogen-heavy fertilizer in late spring (May/June) after Bermuda grass has fully greened up and soil temperatures are consistently above 65°F (18°C)." }, + { 5, "Bermuda Summer Fertilization", 2, "A second application of nitrogen fertilizer in mid-summer (July/August) can further promote vigorous growth for Bermuda grass." }, + { 6, "Pre-Emergent Weed Control", 3, "Apply a pre-emergent herbicide for summer annual weeds like crabgrass in early spring (March-April) before soil temperatures consistently reach 50-55°F (10-13°C) in Oklahoma." }, + { 7, "Post-Emergent Weed Control", 3, "Treat broadleaf weeds in Bermuda grass as needed throughout the growing season with a selective post-emergent herbicide. Always follow product instructions carefully." }, + { 8, "Aeration for Bermuda", 4, "Aerate Bermuda grass in late spring to early summer when it's actively growing. This helps reduce compaction and improve nutrient absorption." }, + { 9, "Bermuda Winter Dormancy", 5, "Bermuda grass will go dormant and turn brown in winter; this is normal. Avoid heavy foot traffic when it is dormant to prevent damage." } + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + // Remove the seeded data if the migration is reverted + migrationBuilder.DeleteData( + table: "LawnCareTips", + keyColumn: "Id", + keyValue: 1); + migrationBuilder.DeleteData( + table: "LawnCareTips", + keyColumn: "Id", + keyValue: 2); + migrationBuilder.DeleteData( + table: "LawnCareTips", + keyColumn: "Id", + keyValue: 3); + migrationBuilder.DeleteData( + table: "LawnCareTips", + keyColumn: "Id", + keyValue: 4); + migrationBuilder.DeleteData( + table: "LawnCareTips", + keyColumn: "Id", + keyValue: 5); + migrationBuilder.DeleteData( + table: "LawnCareTips", + keyColumn: "Id", + keyValue: 6); + migrationBuilder.DeleteData( + table: "LawnCareTips", + keyColumn: "Id", + keyValue: 7); + migrationBuilder.DeleteData( + table: "LawnCareTips", + keyColumn: "Id", + keyValue: 8); + migrationBuilder.DeleteData( + table: "LawnCareTips", + keyColumn: "Id", + keyValue: 9); + } + } +} \ No newline at end of file diff --git a/Migrations/ApplicationDbContextModelSnapshot.cs b/Migrations/ApplicationDbContextModelSnapshot.cs index 65796d4..f6d0c56 100644 --- a/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Migrations/ApplicationDbContextModelSnapshot.cs @@ -40,6 +40,29 @@ namespace turf_tasker.Migrations 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/Models/LawnCareTip.cs b/Models/LawnCareTip.cs new file mode 100644 index 0000000..1b95aba --- /dev/null +++ b/Models/LawnCareTip.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; + +namespace turf_tasker.Models; + +public enum TipCategory +{ + Mowing, + Watering, + Fertilizing, + [Display(Name = "Weed Control")] + WeedControl, + Aeration, + General +} + +public class LawnCareTip +{ + public int Id { get; set; } + + [Required] + [StringLength(100)] + public string Title { get; set; } = string.Empty; + + [Required] + public TipCategory Category { get; set; } + + [Required] + [DataType(DataType.MultilineText)] + public string Content { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/Views/Home/Index.cshtml b/Views/Home/Index.cshtml index 4156a76..ce636ac 100644 --- a/Views/Home/Index.cshtml +++ b/Views/Home/Index.cshtml @@ -90,16 +90,17 @@ -
-
-
-
Log a New Activity
-

Keep your dashboard up to date.

- - Add New Event - +
+
+
+
+
Log a New Activity
+

Keep your dashboard up to date.

+ + Add New Event + +
-
\ No newline at end of file diff --git a/Views/LawnCareTip/Create.cshtml b/Views/LawnCareTip/Create.cshtml new file mode 100644 index 0000000..1da7769 --- /dev/null +++ b/Views/LawnCareTip/Create.cshtml @@ -0,0 +1,40 @@ +@model turf_tasker.Models.LawnCareTip + +@{ + ViewData["Title"] = "Create"; +} + +

Create

+ +

LawnCareTip

+
+
+
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+
+
+
+ + + diff --git a/Views/LawnCareTip/Delete.cshtml b/Views/LawnCareTip/Delete.cshtml new file mode 100644 index 0000000..c7da07c --- /dev/null +++ b/Views/LawnCareTip/Delete.cshtml @@ -0,0 +1,39 @@ +@model turf_tasker.Models.LawnCareTip + +@{ + ViewData["Title"] = "Delete"; +} + +

Delete

+ +

Are you sure you want to delete this?

+
+

LawnCareTip

+
+
+
+ @Html.DisplayNameFor(model => model.Title) +
+
+ @Html.DisplayFor(model => model.Title) +
+
+ @Html.DisplayNameFor(model => model.Category) +
+
+ @Html.DisplayFor(model => model.Category) +
+
+ @Html.DisplayNameFor(model => model.Content) +
+
+ @Html.DisplayFor(model => model.Content) +
+
+ +
+ + | + Back to List +
+
diff --git a/Views/LawnCareTip/Details.cshtml b/Views/LawnCareTip/Details.cshtml new file mode 100644 index 0000000..75f402f --- /dev/null +++ b/Views/LawnCareTip/Details.cshtml @@ -0,0 +1,36 @@ +@model turf_tasker.Models.LawnCareTip + +@{ + ViewData["Title"] = "Details"; +} + +

Details

+ +
+

LawnCareTip

+
+
+
+ @Html.DisplayNameFor(model => model.Title) +
+
+ @Html.DisplayFor(model => model.Title) +
+
+ @Html.DisplayNameFor(model => model.Category) +
+
+ @Html.DisplayFor(model => model.Category) +
+
+ @Html.DisplayNameFor(model => model.Content) +
+
+ @Html.DisplayFor(model => model.Content) +
+
+
+ diff --git a/Views/LawnCareTip/Edit.cshtml b/Views/LawnCareTip/Edit.cshtml new file mode 100644 index 0000000..616a44a --- /dev/null +++ b/Views/LawnCareTip/Edit.cshtml @@ -0,0 +1,41 @@ +@model turf_tasker.Models.LawnCareTip + +@{ + ViewData["Title"] = "Edit"; +} + +

Edit

+ +

LawnCareTip

+
+
+
+
+
+ +
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+
+
+
+ + + diff --git a/Views/LawnCareTip/Index.cshtml b/Views/LawnCareTip/Index.cshtml new file mode 100644 index 0000000..694efe3 --- /dev/null +++ b/Views/LawnCareTip/Index.cshtml @@ -0,0 +1,47 @@ +@model IEnumerable + +@{ + ViewData["Title"] = "Index"; +} + +

Index

+ +

+ Create New +

+ + + + + + + + + + +@foreach (var item in Model) { + + + + + + +} + +
+ @Html.DisplayNameFor(model => model.Title) + + @Html.DisplayNameFor(model => model.Category) + + @Html.DisplayNameFor(model => model.Content) +
+ @Html.DisplayFor(modelItem => item.Title) + + @Html.DisplayFor(modelItem => item.Category) + + @Html.DisplayFor(modelItem => item.Content) + + Edit | + Details | + Delete +
diff --git a/Views/Shared/_Layout.cshtml b/Views/Shared/_Layout.cshtml index 7170ee5..f73bc46 100644 --- a/Views/Shared/_Layout.cshtml +++ b/Views/Shared/_Layout.cshtml @@ -12,7 +12,6 @@