A set of technologies in .NET for building web applications and web services. Miscellaneous topics that do not fit into specific categories.
Hi @Kmcnet ,
Thanks for reaching out.
I would keep your appointment entity as-is and avoid trying to make it dynamically change shape. The entity should just represent the appointment record itself. For the calendar display, the cleaner approach is to build a separate view model that represents the day as a grid.
That usually works well as a list of providers for the columns and a list of 20-minute time slots for the rows. Each row can hold a dictionary keyed by ProviderID, where the value is the appointment for that provider at that time, or null if that cell is empty.
The code below is just meant as a reference pattern, so you may need to adjust the naming or structure a little to fit the way your own project is set up.
Something like this:
public class CalendarDayViewModel
{
public DateTime Date { get; set; }
public List<ProviderColumnViewModel> Providers { get; set; } = new();
public List<CalendarSlotViewModel> Slots { get; set; } = new();
}
public class ProviderColumnViewModel
{
public string ProviderId { get; set; } = string.Empty;
public string ProviderName { get; set; } = string.Empty;
}
public class CalendarSlotViewModel
{
public DateTime SlotTime { get; set; }
public Dictionary<string, tblAppointments?> AppointmentsByProvider { get; set; } = new();
}
Then you can build that view model from the appointments for the selected day:
public CalendarDayViewModel BuildCalendarDay(
DateTime date,
IEnumerable<tblAppointments> appointments)
{
var slotLength = TimeSpan.FromMinutes(20);
var start = date.Date.AddHours(8).AddMinutes(30);
var end = date.Date.AddHours(17);
var dayAppointments = appointments
.Where(a => a.ApptDateTime.HasValue &&
a.ApptDateTime.Value.Date == date.Date &&
!string.IsNullOrWhiteSpace(a.ProviderID))
.ToList();
var providers = dayAppointments
.GroupBy(a => new { a.ProviderID, a.ProviderName })
.Select(g => new ProviderColumnViewModel
{
ProviderId = g.Key.ProviderID!,
ProviderName = g.Key.ProviderName ?? g.Key.ProviderID!
})
.OrderBy(p => p.ProviderName)
.ToList();
var model = new CalendarDayViewModel
{
Date = date.Date,
Providers = providers
};
var appointmentLookup = dayAppointments
.Select(a => new
{
Appointment = a,
SlotTime = GetSlotTime(a.ApptDateTime!.Value, start, slotLength)
})
.Where(x => x.SlotTime >= start && x.SlotTime < end)
.GroupBy(x => new { x.Appointment.ProviderID, x.SlotTime })
.ToDictionary(
g => (ProviderId: g.Key.ProviderID!, SlotTime: g.Key.SlotTime),
g => g.First().Appointment);
for (var slotTime = start; slotTime < end; slotTime = slotTime.Add(slotLength))
{
var slot = new CalendarSlotViewModel
{
SlotTime = slotTime
};
foreach (var provider in providers)
{
appointmentLookup.TryGetValue((provider.ProviderId, slotTime), out var appointment);
slot.AppointmentsByProvider[provider.ProviderId] = appointment;
}
model.Slots.Add(slot);
}
return model;
}
private static DateTime GetSlotTime(DateTime appointmentTime, DateTime start, TimeSpan slotLength)
{
if (appointmentTime <= start)
{
return start;
}
var minutesFromStart = (appointmentTime - start).TotalMinutes;
var slotIndex = (int)Math.Floor(minutesFromStart / slotLength.TotalMinutes);
return start.AddMinutes(slotIndex * slotLength.TotalMinutes);
}
Your Razor view can then loop through Model.Providers to create the columns and Model.Slots to create the rows:
<table class="table">
<thead>
<tr>
<th>Time</th>
@foreach (var provider in Model.Providers)
{
<th>@provider.ProviderName</th>
}
</tr>
</thead>
<tbody>
@foreach (var slot in Model.Slots)
{
<tr>
<td>@slot.SlotTime.ToString("h:mm tt")</td>
@foreach (var provider in Model.Providers)
{
var appointment = slot.AppointmentsByProvider[provider.ProviderId];
<td>
@if (appointment != null)
{
<div>
Patient: @appointment.PatientID
Duration: @appointment.AppointmentDuration
</div>
}
</td>
}
</tr>
}
</tbody>
</table>
This way, the number of provider columns can change from day to day without requiring a different model or a DataTable. The providers determine the columns, and the generated time slots determine the rows.
I adjusted the sample above to normalize appointment times into a 20-minute slot before matching them, which is a little safer than relying on an exact DateTime equality check. I would also consider storing AppointmentDuration as an int number of minutes, or possibly a TimeSpan, instead of a string, especially if you may later need appointments to span multiple slots.
Hope this helps! If my answer was helpful, I would greatly appreciate it if you could follow the instructions here so others with the same problem can benefit as well.