feat: Enhance UI/UX across multiple views with consistent styling and improved layout #3

Merged
ciphervance merged 6 commits from feat/calendar-view into main 2025-06-21 21:31:00 +00:00
16 changed files with 609 additions and 60 deletions

View file

@ -14,19 +14,15 @@ namespace turf_tasker.Controllers
_context = context;
}
// GET: LawnCareEvents
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> Create([Bind("Id,EventType,EventDate,MowingPattern,Notes")] LawnCareEvent lawnCareEvent)
@ -96,7 +86,6 @@ namespace turf_tasker.Controllers
return View(lawnCareEvent);
}
// GET: LawnCareEvents/Edit/5
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<JsonResult> 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);
}
}
}

View file

@ -0,0 +1,72 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("EventDate")
.HasColumnType("TEXT");
b.Property<int>("EventType")
.HasColumnType("INTEGER");
b.Property<int?>("MowingPattern")
.HasColumnType("INTEGER");
b.Property<string>("Notes")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("LawnCareEvents");
});
modelBuilder.Entity("turf_tasker.Models.LawnCareTip", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Category")
.HasColumnType("INTEGER");
b.Property<string>("Content")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("LawnCareTips");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace turf_tasker.Migrations
{
/// <inheritdoc />
public partial class AddNotApplicableMowingPattern : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View file

@ -0,0 +1,86 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<decimal?>("AppliedAmount")
.HasColumnType("TEXT");
b.Property<DateTime>("EventDate")
.HasColumnType("TEXT");
b.Property<int>("EventType")
.HasColumnType("INTEGER");
b.Property<decimal?>("MowerHeightInches")
.HasColumnType("TEXT");
b.Property<int?>("MowingPattern")
.HasColumnType("INTEGER");
b.Property<string>("Notes")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<string>("ProblemObserved")
.HasMaxLength(250)
.HasColumnType("TEXT");
b.Property<string>("ProductUsed")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("LawnCareEvents");
});
modelBuilder.Entity("turf_tasker.Models.LawnCareTip", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Category")
.HasColumnType("INTEGER");
b.Property<string>("Content")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("LawnCareTips");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,60 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace turf_tasker.Migrations
{
/// <inheritdoc />
public partial class AddEventDetails : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<decimal>(
name: "AppliedAmount",
table: "LawnCareEvents",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<decimal>(
name: "MowerHeightInches",
table: "LawnCareEvents",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "ProblemObserved",
table: "LawnCareEvents",
type: "TEXT",
maxLength: 250,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "ProductUsed",
table: "LawnCareEvents",
type: "TEXT",
maxLength: 200,
nullable: true);
}
/// <inheritdoc />
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");
}
}
}

View file

@ -23,12 +23,18 @@ namespace turf_tasker.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<decimal?>("AppliedAmount")
.HasColumnType("TEXT");
b.Property<DateTime>("EventDate")
.HasColumnType("TEXT");
b.Property<int>("EventType")
.HasColumnType("INTEGER");
b.Property<decimal?>("MowerHeightInches")
.HasColumnType("TEXT");
b.Property<int?>("MowingPattern")
.HasColumnType("INTEGER");
@ -36,6 +42,14 @@ namespace turf_tasker.Migrations
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<string>("ProblemObserved")
.HasMaxLength(250)
.HasColumnType("TEXT");
b.Property<string>("ProductUsed")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("LawnCareEvents");

View file

@ -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,
@ -30,17 +36,30 @@ public class LawnCareEvent
[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; }
}

View file

@ -0,0 +1,137 @@
@{
ViewData["Title"] = "Lawn Care Calendar";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>@ViewData["Title"]</h1>
<a asp-action="Create" asp-controller="LawnCareEvents" class="btn btn-primary">
<i class="bi bi-plus-circle-fill"></i> Create New Event
</a>
</div>
<div class="calendar-container">
<div class="calendar-header">
<button id="prevMonth" class="btn btn-secondary">&lt; Prev</button>
<h2 id="currentMonthYear"></h2>
<button id="nextMonth" class="btn btn-secondary">Next &gt;</button>
</div>
<div class="calendar-grid">
<div class="day-name">Sun</div>
<div class="day-name">Mon</div>
<div class="day-name">Tue</div>
<div class="day-name">Wed</div>
<div class="day-name">Thu</div>
<div class="day-name">Fri</div>
<div class="day-name">Sat</div>
</div>
<div id="calendarDays" class="calendar-grid">
</div>
</div>
@section Scripts {
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function () {
const calendarDaysEl = $('#calendarDays');
const currentMonthYearEl = $('#currentMonthYear');
const prevMonthBtn = $('#prevMonth');
const nextMonthBtn = $('#nextMonth');
let currentMonth = new Date().getMonth();
let currentYear = new Date().getFullYear();
let allEvents = [];
function renderCalendar(month, year) {
calendarDaysEl.empty();
currentMonthYearEl.text(new Date(year, month).toLocaleString('default', { month: 'long', year: 'numeric' }));
const firstDayOfMonth = new Date(year, month, 1);
const lastDayOfMonth = new Date(year, month + 1, 0);
const daysInMonth = lastDayOfMonth.getDate();
const startDayOfWeek = firstDayOfMonth.getDay();
const prevMonthLastDay = new Date(year, month, 0).getDate();
for (let i = startDayOfWeek; i > 0; i--) {
const dayNum = prevMonthLastDay - i + 1;
calendarDaysEl.append(`<div class="calendar-day other-month"><span class="day-number">${dayNum}</span></div>`);
}
for (let day = 1; day <= daysInMonth; day++) {
const date = new Date(year, month, day);
const dateString = date.toISOString().split('T')[0];
let dayHtml = `<div class="calendar-day" data-date="${dateString}">`;
dayHtml += `<span class="day-number">${day}</span>`;
const eventsOnThisDay = allEvents.filter(event => event.start === dateString);
eventsOnThisDay.forEach(event => {
const typeClass = event.title.split(' ')[0].toLowerCase().replace(/[^a-z0-9]/g, '');
dayHtml += `<div class="event-indicator ${typeClass}" data-event-id="${event.id}" data-event-url="${event.url}">${event.title}</div>`;
});
dayHtml += `</div>`;
calendarDaysEl.append(dayHtml);
}
const totalDaysDisplayed = startDayOfWeek + daysInMonth;
const remainingCells = 42 - totalDaysDisplayed;
for (let i = 1; i <= remainingCells && totalDaysDisplayed + i <= 42; i++) {
calendarDaysEl.append(`<div class="calendar-day other-month"><span class="day-number">${i}</span></div>`);
}
$('.calendar-day .event-indicator').on('click', function (e) {
e.stopPropagation();
const url = $(this).data('event-url');
if (url) {
window.location.href = url;
}
});
$('.calendar-day:not(.other-month)').on('click', function () {
const date = $(this).data('date');
window.location.href = `/LawnCareEvents/Create?date=${date}`;
});
}
async function fetchEvents() {
try {
const response = await fetch('/LawnCareEvents/GetEventsJson');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
allEvents = await response.json();
renderCalendar(currentMonth, currentYear);
} catch (error) {
console.error('Error fetching events:', error);
}
}
prevMonthBtn.on('click', function () {
currentMonth--;
if (currentMonth < 0) {
currentMonth = 11;
currentYear--;
}
renderCalendar(currentMonth, currentYear);
});
nextMonthBtn.on('click', function () {
currentMonth++;
if (currentMonth > 11) {
currentMonth = 0;
currentYear++;
}
renderCalendar(currentMonth, currentYear);
});
fetchEvents();
});
</script>
}
@section Styles {
<link href="~/css/calendar.css" rel="stylesheet" asp-append-version="true" />
}

View file

@ -39,7 +39,11 @@
<form asp-action="Delete">
<input type="hidden" asp-for="Id" />
<input type="submit" value="Delete" class="btn btn-danger" /> |
<a asp-action="Index">Back to List</a>
<input type="submit" value="Delete" class="btn btn-danger" />
</form>
<div class="mt-3">
<a asp-action="Index">Back to List</a>
</div>
</div>

View file

@ -43,6 +43,7 @@
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
<a asp-action="Delete" asp-route-id="@Model.Id" class="btn btn-danger ms-2">Delete</a>
</div>
</form>
</div>

View file

@ -1,55 +1,57 @@
@model turf_tasker.Models.LawnCareEvent
@{
ViewData["Title"] = "Edit";
ViewData["Title"] = "Edit";
}
<h1>Edit</h1>
<h4>LawnCareEvent</h4>
<h4>Lawn Care Event</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Edit">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Id" />
<div class="form-group">
<div class="form-group mb-3">
<label asp-for="EventType" class="control-label"></label>
<select asp-for="EventType" class="form-control" asp-items="Html.GetEnumSelectList<LawnCareEventType>()"></select>
<span asp-validation-for="EventType" class="text-danger"></span>
</div>
<div class="form-group">
<div class="form-group mb-3">
<label asp-for="EventDate" class="control-label"></label>
<input asp-for="EventDate" class="form-control" />
<span asp-validation-for="EventDate" class="text-danger"></span>
</div>
<div id="mowingPatternGroup">
<div class="form-group">
<div class="form-group mb-3">
<label asp-for="MowingPattern" class="control-label"></label>
<select asp-for="MowingPattern" class="form-control" asp-items="Html.GetEnumSelectList<MowingPattern>()"></select>
<span asp-validation-for="MowingPattern" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="form-group mb-3">
<label asp-for="Notes" class="control-label"></label>
<textarea asp-for="Notes" class="form-control"></textarea>
<span asp-validation-for="Notes" class="text-danger"></span>
</div>
<div class="form-group mt-3">
<input type="submit" value="Save" class="btn btn-primary" />
<!-- NEW: Add a space/margin and use Razor syntax to pass the Id -->
<a asp-action="Delete" asp-route-id="@Model.Id" class="btn btn-danger">Delete</a>
</div>
</form>
</div>
</div>
<div>
<div class="mt-3">
<a asp-action="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
<script type="text/javascript">
<script type="text/javascript">
$(document).ready(function () {
function toggleMowingPattern() {
@ -68,5 +70,5 @@
toggleMowingPattern();
});
});
</script>
</script>
}

View file

@ -4,11 +4,14 @@
ViewData["Title"] = "Lawn Care Log";
}
<h1>@ViewData["Title"]</h1>
<div class="text-center">
<h1>@ViewData["Title"]</h1>
<p>
<p>
<a asp-action="Create" class="btn btn-primary">Create New Event</a>
</p>
<a asp-action="Calendar" class="btn btn-info ms-2">View Calendar</a>
</p>
</div>
<!-- Add the search form -->
<form asp-action="Index" method="get">
@ -16,8 +19,8 @@
<p>
Find by type or note:
<input type="text" name="searchString" value="@ViewData["CurrentFilter"]" />
<input type="submit" value="Search" class="btn btn-secondary" /> |
<a asp-action="Index">Back to Full List</a>
<input type="submit" value="Search" class="btn btn-secondary" />
<a asp-action="Index" class="btn btn-primary">Reset Filter</a>
</p>
</div>
</form>
@ -63,9 +66,11 @@
@Html.DisplayFor(modelItem => item.Notes)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
<a asp-action="Details" asp-route-id="@item.Id">Details</a> |
<a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
<div class="d-flex flex-wrap gap-2">
<a asp-action="Edit" asp-route-id="@item.Id" class="btn btn-sm btn-outline-primary">Edit</a>
<a asp-action="Details" asp-route-id="@item.Id" class="btn btn-sm btn-outline-info">Details</a>
<a asp-action="Delete" asp-route-id="@item.Id" class="btn btn-sm btn-outline-danger">Delete</a>
</div>
</td>
</tr>
}

View file

@ -7,7 +7,7 @@
<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
<a asp-action="Create" class="btn btn-primary">Create New Tip</a>
</p>
<table class="table">
<thead>
@ -36,10 +36,11 @@
<td>
@Html.DisplayFor(modelItem => item.Content)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
<a asp-action="Details" asp-route-id="@item.Id">Details</a> |
<a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
<td class="col-actions">
<div class="d-flex flex-wrap gap-2">
<a asp-action="Edit" asp-route-id="@item.Id" class="btn btn-sm btn-outline-primary">Edit</a>
<a asp-action="Delete" asp-route-id="@item.Id" class="btn btn-sm btn-outline-danger">Delete</a>
</div>
</td>
</tr>
}

View file

@ -5,6 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - MowLog</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
@await RenderSectionAsync("Scripts", required: false)
@await RenderSectionAsync("Styles", required: false)
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/MowLog.styles.css" asp-append-version="true" />
</head>
@ -46,6 +48,5 @@
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>

95
wwwroot/css/calendar.css Normal file
View file

@ -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;
}

View file

@ -34,3 +34,16 @@ 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 */
}