938 lines
30 KiB
C#
938 lines
30 KiB
C#
using plant_manager.Data.Models;
|
|
|
|
namespace plant_manager
|
|
{
|
|
public record CreatePlantRequest(
|
|
string Nickname,
|
|
DateOnly? Birthday,
|
|
int? TaxonId,
|
|
int? LocationId,
|
|
IReadOnlyList<SavePlantCareScheduleRequest>? CareSchedules);
|
|
|
|
public record UpdatePlantRequest(
|
|
string Nickname,
|
|
DateOnly? Birthday,
|
|
int? TaxonId,
|
|
int? LocationId,
|
|
IReadOnlyList<SavePlantCareScheduleRequest>? CareSchedules);
|
|
|
|
public record SavePlantCareScheduleRequest(
|
|
int CareActivityId,
|
|
IReadOnlyList<int>? PlantIds,
|
|
int? EveryDays,
|
|
DateOnly? ScheduledFor,
|
|
string? RecurrenceMode,
|
|
int? RepeatEvery,
|
|
string? RepeatUnit,
|
|
string? RepeatOnDays,
|
|
string? EndsMode,
|
|
DateOnly? EndsOn,
|
|
int? EndsAfterOccurrences);
|
|
|
|
public record BulkSavePlantCareScheduleRequest(
|
|
IReadOnlyList<int> PlantIds,
|
|
int CareActivityId,
|
|
int? EveryDays,
|
|
DateOnly? ScheduledFor,
|
|
string? RecurrenceMode,
|
|
int? RepeatEvery,
|
|
string? RepeatUnit,
|
|
string? RepeatOnDays,
|
|
string? EndsMode,
|
|
DateOnly? EndsOn,
|
|
int? EndsAfterOccurrences);
|
|
|
|
public record ImportPlantTaxonRequest(
|
|
string Source,
|
|
string ExternalId,
|
|
string ScientificName,
|
|
string? CanonicalName,
|
|
string? CommonName,
|
|
string? Rank,
|
|
string? Status,
|
|
string? Family,
|
|
string? Genus,
|
|
string? Species);
|
|
|
|
public record SavePlantLocationRequest(
|
|
string Name,
|
|
string? Notes);
|
|
|
|
public record SavePlantGroupRequest(
|
|
string Name,
|
|
IReadOnlyList<int>? PlantIds,
|
|
string? Notes);
|
|
|
|
public record SaveCareActionRequest(
|
|
string Name,
|
|
string? Description);
|
|
|
|
public record SaveActionResourceRequest(
|
|
string Name,
|
|
string? Notes);
|
|
|
|
public record SaveCareActivityRequest(
|
|
string Name,
|
|
IReadOnlyList<SaveCareActivityActionRequest> Actions,
|
|
string? Notes);
|
|
|
|
public record SaveCareActivityActionRequest(
|
|
int CareActionId,
|
|
IReadOnlyList<SaveCareActivityActionResourceRequest>? Resources);
|
|
|
|
public record SaveCareActivityActionResourceRequest(
|
|
int ActionResourceId,
|
|
decimal? Quantity,
|
|
string? Unit,
|
|
string? Notes);
|
|
|
|
public record SaveRecipeRequest(
|
|
string Name,
|
|
string? MeasurementMode,
|
|
string? OutputResourceName,
|
|
IReadOnlyList<SaveRecipeComponentRequest>? Components,
|
|
string? Notes);
|
|
|
|
public record SaveRecipeComponentRequest(
|
|
int ActionResourceId,
|
|
decimal? Quantity,
|
|
string? Unit,
|
|
string? Notes);
|
|
|
|
public record CareActivityRecipeComponentDto(
|
|
int ActionResourceId,
|
|
string Name,
|
|
decimal? Quantity,
|
|
string? Unit,
|
|
string? Notes,
|
|
int SortOrder)
|
|
{
|
|
public static CareActivityRecipeComponentDto FromRecipeComponent(RecipeComponent component) =>
|
|
new(
|
|
component.ActionResourceId,
|
|
component.ActionResource.Name,
|
|
component.Quantity,
|
|
component.Unit,
|
|
component.Notes,
|
|
component.SortOrder);
|
|
}
|
|
|
|
public record CareActivityRecipeDto(
|
|
int Id,
|
|
string Name,
|
|
string MeasurementMode,
|
|
IReadOnlyList<CareActivityRecipeComponentDto> Components,
|
|
string? Notes)
|
|
{
|
|
public static CareActivityRecipeDto FromRecipe(Recipe recipe) =>
|
|
new(
|
|
recipe.Id,
|
|
recipe.Name,
|
|
recipe.MeasurementMode,
|
|
recipe.Components
|
|
.OrderBy(component => component.SortOrder)
|
|
.Select(CareActivityRecipeComponentDto.FromRecipeComponent)
|
|
.ToList(),
|
|
recipe.Notes);
|
|
}
|
|
|
|
public record CareActivityActionResourceDto(
|
|
int ActionResourceId,
|
|
string Name,
|
|
decimal? Quantity,
|
|
string? Unit,
|
|
string? Notes,
|
|
CareActivityRecipeDto? ProducedByRecipe)
|
|
{
|
|
public static CareActivityActionResourceDto FromCareActivityActionResource(
|
|
CareActivityActionResource resource) =>
|
|
new(
|
|
resource.ActionResourceId,
|
|
resource.ActionResource.Name,
|
|
resource.Quantity,
|
|
resource.Unit,
|
|
resource.Notes,
|
|
resource.ActionResource.ProducedByRecipe is null
|
|
? null
|
|
: CareActivityRecipeDto.FromRecipe(resource.ActionResource.ProducedByRecipe));
|
|
}
|
|
|
|
public record CareActivityActionDto(
|
|
int CareActionId,
|
|
string Name,
|
|
string? Description,
|
|
int SortOrder,
|
|
IReadOnlyList<CareActivityActionResourceDto> Resources)
|
|
{
|
|
public static CareActivityActionDto FromCareActivityAction(CareActivityAction activityAction) =>
|
|
new(
|
|
activityAction.CareActionId,
|
|
activityAction.CareAction.Name,
|
|
activityAction.CareAction.Description,
|
|
activityAction.SortOrder,
|
|
activityAction.Resources
|
|
.OrderBy(resource => resource.ActionResource.Name)
|
|
.Select(CareActivityActionResourceDto.FromCareActivityActionResource)
|
|
.ToList());
|
|
}
|
|
|
|
public record SavePlantFlagDefinitionRequest(
|
|
string Name,
|
|
string? Color);
|
|
|
|
public record AssignPlantFlagRequest(
|
|
int PlantFlagDefinitionId,
|
|
DateOnly? StartedOn,
|
|
string? Notes);
|
|
|
|
public record UpdatePlantFlagRequest(
|
|
DateOnly? StartedOn,
|
|
DateOnly? ResolvedOn,
|
|
string? Notes);
|
|
|
|
public record CreateActionLogRequest(
|
|
int PlantId,
|
|
int CareActivityId,
|
|
string? Notes,
|
|
DateOnly? PerformedOn,
|
|
IReadOnlyList<ActionLogResourceRequest>? Resources);
|
|
|
|
public record UpdateActionLogRequest(
|
|
int PlantId,
|
|
int CareActivityId,
|
|
string? Notes,
|
|
DateOnly PerformedOn,
|
|
IReadOnlyList<ActionLogResourceRequest>? Resources);
|
|
|
|
public record ActionLogResourceRequest(
|
|
int ActionResourceId,
|
|
decimal? Quantity,
|
|
string? Unit);
|
|
|
|
public record BulkCompleteCareTasksRequest(
|
|
int CareActivityId,
|
|
IReadOnlyList<int> PlantIds,
|
|
DateOnly? PerformedOn,
|
|
string? Notes,
|
|
IReadOnlyList<ActionLogResourceRequest>? Resources);
|
|
|
|
public record DismissCareTasksRequest(
|
|
int CareActivityId,
|
|
IReadOnlyList<int> PlantIds,
|
|
DateOnly? DismissedOn,
|
|
string? Notes);
|
|
|
|
public record SnoozeCareTasksRequest(
|
|
int CareActivityId,
|
|
IReadOnlyList<int> PlantIds,
|
|
DateOnly SnoozedUntil,
|
|
string? Notes);
|
|
|
|
public record UpdateCareDismissalRequest(
|
|
int PlantId,
|
|
int CareActivityId,
|
|
DateOnly DismissedOn,
|
|
string? Notes);
|
|
|
|
public record UpdateCareSnoozeRequest(
|
|
int PlantId,
|
|
int CareActivityId,
|
|
DateOnly SnoozedUntil,
|
|
string? Notes);
|
|
|
|
public record CatalogImportIssue(
|
|
string Sheet,
|
|
int Row,
|
|
string Field,
|
|
string Message,
|
|
string Severity);
|
|
|
|
public record CatalogImportSheetSummary(
|
|
string Sheet,
|
|
int Rows,
|
|
int Creates,
|
|
int Updates,
|
|
int Skips);
|
|
|
|
public record CatalogImportResult(
|
|
bool Applied,
|
|
bool CanApply,
|
|
int Created,
|
|
int Updated,
|
|
int Skipped,
|
|
IReadOnlyList<CatalogImportSheetSummary> Sheets,
|
|
IReadOnlyList<CatalogImportIssue> Issues);
|
|
|
|
public record PlantTaxonDto(
|
|
int Id,
|
|
string Name,
|
|
string Genus,
|
|
string Species,
|
|
string? Cultivar,
|
|
string? Variety,
|
|
string? Authority,
|
|
string? Family,
|
|
string? CommonName,
|
|
string? ExternalSource,
|
|
string? ExternalId)
|
|
{
|
|
public static PlantTaxonDto FromTaxon(PlantTaxon taxon) =>
|
|
new(
|
|
taxon.Id,
|
|
taxon.Name,
|
|
taxon.Genus,
|
|
taxon.Species,
|
|
taxon.Cultivar,
|
|
taxon.Variety,
|
|
taxon.Authority,
|
|
taxon.Family,
|
|
taxon.CommonName,
|
|
taxon.ExternalSource,
|
|
taxon.ExternalId);
|
|
}
|
|
|
|
public record PlantLocationDto(
|
|
int Id,
|
|
string Name,
|
|
string? Notes)
|
|
{
|
|
public static PlantLocationDto FromLocation(PlantLocation location) =>
|
|
new(location.Id, location.Name, location.Notes);
|
|
}
|
|
|
|
public record PlantGroupSummaryDto(
|
|
int Id,
|
|
string Name)
|
|
{
|
|
public static PlantGroupSummaryDto FromMembership(PlantGroupMembership membership) =>
|
|
new(membership.PlantGroupId, membership.PlantGroup.Name);
|
|
}
|
|
|
|
public record PlantGroupMemberDto(
|
|
int Id,
|
|
string Nickname)
|
|
{
|
|
public static PlantGroupMemberDto FromMembership(PlantGroupMembership membership) =>
|
|
new(membership.PlantId, membership.Plant.Nickname);
|
|
}
|
|
|
|
public record PlantGroupDto(
|
|
int Id,
|
|
string Name,
|
|
IReadOnlyList<PlantGroupMemberDto> Plants,
|
|
string? Notes)
|
|
{
|
|
public static PlantGroupDto FromGroup(PlantGroup group) =>
|
|
new(
|
|
group.Id,
|
|
group.Name,
|
|
group.Memberships
|
|
.OrderBy(membership => membership.Plant.Nickname)
|
|
.Select(PlantGroupMemberDto.FromMembership)
|
|
.ToList(),
|
|
group.Notes);
|
|
}
|
|
|
|
public record CareActionDto(
|
|
int Id,
|
|
string Name,
|
|
string? Description)
|
|
{
|
|
public static CareActionDto FromCareAction(CareAction action) =>
|
|
new(action.Id, action.Name, action.Description);
|
|
}
|
|
|
|
public record ActionResourceDto(
|
|
int Id,
|
|
string Name,
|
|
string? Notes,
|
|
RecipeSummaryDto? ProducedByRecipe)
|
|
{
|
|
public static ActionResourceDto FromActionResource(ActionResource resource) =>
|
|
new(
|
|
resource.Id,
|
|
resource.Name,
|
|
resource.Notes,
|
|
resource.ProducedByRecipe is null
|
|
? null
|
|
: RecipeSummaryDto.FromRecipe(resource.ProducedByRecipe));
|
|
}
|
|
|
|
public record ActionResourceSummaryDto(
|
|
int Id,
|
|
string Name)
|
|
{
|
|
public static ActionResourceSummaryDto FromActionResource(ActionResource resource) =>
|
|
new(resource.Id, resource.Name);
|
|
}
|
|
|
|
public record RecipeSummaryDto(
|
|
int Id,
|
|
string Name,
|
|
string MeasurementMode)
|
|
{
|
|
public static RecipeSummaryDto FromRecipe(Recipe recipe) =>
|
|
new(recipe.Id, recipe.Name, recipe.MeasurementMode);
|
|
}
|
|
|
|
public record CareActivityDto(
|
|
int Id,
|
|
string Name,
|
|
int CareActionId,
|
|
string Action,
|
|
IReadOnlyList<CareActivityActionDto> Actions,
|
|
string? Notes)
|
|
{
|
|
public static CareActivityDto FromCareActivity(CareActivity activity) =>
|
|
new(
|
|
activity.Id,
|
|
activity.Name,
|
|
activity.PrimaryAction()?.Id ?? 0,
|
|
activity.PrimaryAction()?.Name ?? activity.Name,
|
|
activity.Actions
|
|
.OrderBy(action => action.SortOrder)
|
|
.Select(CareActivityActionDto.FromCareActivityAction)
|
|
.ToList(),
|
|
activity.Notes);
|
|
}
|
|
|
|
public record RecipeComponentDto(
|
|
int ActionResourceId,
|
|
string Name,
|
|
decimal? Quantity,
|
|
string? Unit,
|
|
string? Notes,
|
|
int SortOrder)
|
|
{
|
|
public static RecipeComponentDto FromRecipeComponent(RecipeComponent component) =>
|
|
new(
|
|
component.ActionResourceId,
|
|
component.ActionResource.Name,
|
|
component.Quantity,
|
|
component.Unit,
|
|
component.Notes,
|
|
component.SortOrder);
|
|
}
|
|
|
|
public record RecipeDto(
|
|
int Id,
|
|
string Name,
|
|
string MeasurementMode,
|
|
ActionResourceSummaryDto? OutputResource,
|
|
IReadOnlyList<RecipeComponentDto> Components,
|
|
string? Notes)
|
|
{
|
|
public static RecipeDto FromRecipe(Recipe recipe) =>
|
|
new(
|
|
recipe.Id,
|
|
recipe.Name,
|
|
recipe.MeasurementMode,
|
|
recipe.OutputResource is null
|
|
? null
|
|
: ActionResourceSummaryDto.FromActionResource(recipe.OutputResource),
|
|
recipe.Components
|
|
.OrderBy(component => component.SortOrder)
|
|
.Select(RecipeComponentDto.FromRecipeComponent)
|
|
.ToList(),
|
|
recipe.Notes);
|
|
}
|
|
|
|
public record PlantDto(
|
|
int Id,
|
|
string Nickname,
|
|
DateOnly? Birthday,
|
|
int? TaxonId,
|
|
string Taxon,
|
|
int? LocationId,
|
|
string Location,
|
|
string NextCare,
|
|
string Status,
|
|
IReadOnlyList<PlantFlagDto> Flags,
|
|
IReadOnlyList<PlantGroupSummaryDto> Groups,
|
|
IReadOnlyList<ActionLogDto> ActionLogs,
|
|
IReadOnlyList<PlantCareScheduleDto> CareSchedules)
|
|
{
|
|
public static PlantDto FromPlant(Plant plant)
|
|
{
|
|
var today = DateOnly.FromDateTime(DateTime.UtcNow);
|
|
var schedules = plant.CareScheduleAssignments
|
|
.OrderBy(assignment => assignment.PlantCareSchedule.CareActivity.Name)
|
|
.Select(assignment => PlantCareScheduleDto.FromAssignment(
|
|
assignment,
|
|
GetLatestCareEventOn(plant, assignment.PlantCareSchedule.CareActivityId),
|
|
today))
|
|
.ToList();
|
|
var nextCare = schedules
|
|
.Select(schedule =>
|
|
{
|
|
var source = plant.CareScheduleAssignments
|
|
.Select(assignment => assignment.PlantCareSchedule)
|
|
.First(item => item.Id == schedule.Id);
|
|
return PlantCareFormatter.GetNextCareDate(
|
|
source,
|
|
schedule.LastPerformedOn,
|
|
GetCompletedOccurrences(plant, source.CareActivityId),
|
|
GetSnoozedUntil(plant, source.CareActivityId));
|
|
})
|
|
.Where(date => date is not null)
|
|
.OrderBy(date => date)
|
|
.FirstOrDefault();
|
|
|
|
return new PlantDto(
|
|
plant.Id,
|
|
plant.Nickname,
|
|
plant.Birthday,
|
|
plant.TaxonId,
|
|
plant.Taxon is null ? "Unassigned" : $"{plant.Taxon.Genus} {plant.Taxon.Species}",
|
|
plant.LocationId,
|
|
plant.Location?.Name ?? "Unassigned",
|
|
PlantCareFormatter.FormatRelativeDate(nextCare, today, "Unscheduled"),
|
|
PlantCareFormatter.GetStatus(nextCare, today),
|
|
plant.Flags
|
|
.OrderBy(flag => flag.ResolvedOn is not null)
|
|
.ThenByDescending(flag => flag.StartedOn)
|
|
.ThenBy(flag => flag.Definition.Name)
|
|
.Select(PlantFlagDto.FromPlantFlag)
|
|
.ToList(),
|
|
plant.GroupMemberships
|
|
.OrderBy(membership => membership.PlantGroup.Name)
|
|
.Select(PlantGroupSummaryDto.FromMembership)
|
|
.ToList(),
|
|
plant.ActionLogs
|
|
.OrderByDescending(log => log.PerformedOn)
|
|
.ThenByDescending(log => log.Id)
|
|
.Select(ActionLogDto.FromActionLog)
|
|
.ToList(),
|
|
schedules);
|
|
}
|
|
|
|
private static DateOnly? GetLatestCareEventOn(Plant plant, int careActivityId) =>
|
|
plant.ActionLogs
|
|
.Where(log => log.CareActivityId == careActivityId)
|
|
.Select(log => (DateOnly?)log.PerformedOn)
|
|
.Concat(plant.CareDismissals
|
|
.Where(dismissal => dismissal.CareActivityId == careActivityId)
|
|
.Select(dismissal => (DateOnly?)dismissal.DismissedOn))
|
|
.Max();
|
|
|
|
private static int GetCompletedOccurrences(Plant plant, int careActivityId) =>
|
|
plant.ActionLogs.Count(log => log.CareActivityId == careActivityId)
|
|
+ plant.CareDismissals.Count(dismissal => dismissal.CareActivityId == careActivityId);
|
|
|
|
private static DateOnly? GetSnoozedUntil(Plant plant, int careActivityId) =>
|
|
plant.CareSnoozes
|
|
.Where(snooze => snooze.CareActivityId == careActivityId)
|
|
.Select(snooze => (DateOnly?)snooze.SnoozedUntil)
|
|
.Max();
|
|
}
|
|
|
|
public record PlantCareScheduleDto(
|
|
int Id,
|
|
int CareActivityId,
|
|
int CareActionId,
|
|
string Action,
|
|
int EveryDays,
|
|
DateOnly? ScheduledFor,
|
|
string RecurrenceMode,
|
|
int RepeatEvery,
|
|
string RepeatUnit,
|
|
string? RepeatOnDays,
|
|
string EndsMode,
|
|
DateOnly? EndsOn,
|
|
int? EndsAfterOccurrences,
|
|
DateOnly? LastPerformedOn,
|
|
string LastPerformed,
|
|
string NextCare,
|
|
string Status)
|
|
{
|
|
public static PlantCareScheduleDto FromAssignment(
|
|
PlantCareScheduleAssignment assignment,
|
|
DateOnly? lastPerformedOn,
|
|
DateOnly today)
|
|
{
|
|
var schedule = assignment.PlantCareSchedule;
|
|
var nextCare = PlantCareFormatter.GetNextCareDate(
|
|
schedule,
|
|
lastPerformedOn,
|
|
GetCompletedOccurrences(assignment),
|
|
GetSnoozedUntil(assignment));
|
|
|
|
return new PlantCareScheduleDto(
|
|
schedule.Id,
|
|
schedule.CareActivityId,
|
|
schedule.CareActionId,
|
|
schedule.CareActivity.Name,
|
|
schedule.EveryDays,
|
|
schedule.ScheduledFor,
|
|
schedule.RecurrenceMode,
|
|
schedule.RepeatEvery,
|
|
schedule.RepeatUnit,
|
|
schedule.RepeatOnDays,
|
|
schedule.EndsMode,
|
|
schedule.EndsOn,
|
|
schedule.EndsAfterOccurrences,
|
|
lastPerformedOn,
|
|
PlantCareFormatter.FormatRelativeDate(lastPerformedOn, today, "Never"),
|
|
PlantCareFormatter.FormatRelativeDate(nextCare, today, "Unscheduled"),
|
|
PlantCareFormatter.GetStatus(nextCare, today));
|
|
}
|
|
|
|
private static int GetCompletedOccurrences(PlantCareScheduleAssignment assignment) =>
|
|
assignment.Plant.ActionLogs.Count(log => log.CareActivityId == assignment.PlantCareSchedule.CareActivityId)
|
|
+ assignment.Plant.CareDismissals.Count(dismissal => dismissal.CareActivityId == assignment.PlantCareSchedule.CareActivityId);
|
|
|
|
private static DateOnly? GetSnoozedUntil(PlantCareScheduleAssignment assignment) =>
|
|
assignment.Plant.CareSnoozes
|
|
.Where(snooze => snooze.CareActivityId == assignment.PlantCareSchedule.CareActivityId)
|
|
.Select(snooze => (DateOnly?)snooze.SnoozedUntil)
|
|
.Max();
|
|
}
|
|
|
|
public record PlantCareScheduleAssignmentDto(
|
|
int Id,
|
|
string Nickname)
|
|
{
|
|
public static PlantCareScheduleAssignmentDto FromAssignment(PlantCareScheduleAssignment assignment) =>
|
|
new(assignment.PlantId, assignment.Plant.Nickname);
|
|
}
|
|
|
|
public record PlantCareScheduleRuleDto(
|
|
int Id,
|
|
int CareActivityId,
|
|
int CareActionId,
|
|
string Action,
|
|
int EveryDays,
|
|
DateOnly? ScheduledFor,
|
|
string RecurrenceMode,
|
|
int RepeatEvery,
|
|
string RepeatUnit,
|
|
string? RepeatOnDays,
|
|
string EndsMode,
|
|
DateOnly? EndsOn,
|
|
int? EndsAfterOccurrences,
|
|
IReadOnlyList<PlantCareScheduleAssignmentDto> Plants)
|
|
{
|
|
public static PlantCareScheduleRuleDto FromSchedule(PlantCareSchedule schedule) =>
|
|
new(
|
|
schedule.Id,
|
|
schedule.CareActivityId,
|
|
schedule.CareActionId,
|
|
schedule.CareActivity.Name,
|
|
schedule.EveryDays,
|
|
schedule.ScheduledFor,
|
|
schedule.RecurrenceMode,
|
|
schedule.RepeatEvery,
|
|
schedule.RepeatUnit,
|
|
schedule.RepeatOnDays,
|
|
schedule.EndsMode,
|
|
schedule.EndsOn,
|
|
schedule.EndsAfterOccurrences,
|
|
schedule.Assignments
|
|
.OrderBy(assignment => assignment.Plant.Nickname)
|
|
.Select(PlantCareScheduleAssignmentDto.FromAssignment)
|
|
.ToList());
|
|
}
|
|
|
|
internal static class CareActivityExtensions
|
|
{
|
|
public static CareAction? PrimaryAction(this CareActivity activity) =>
|
|
activity.Actions
|
|
.OrderBy(action => action.SortOrder)
|
|
.Select(action => action.CareAction)
|
|
.FirstOrDefault();
|
|
|
|
public static int PrimaryActionId(this CareActivity activity) =>
|
|
activity.Actions
|
|
.OrderBy(action => action.SortOrder)
|
|
.Select(action => action.CareActionId)
|
|
.FirstOrDefault();
|
|
}
|
|
|
|
public record CareTaskDto(
|
|
int Id,
|
|
int PlantId,
|
|
string PlantName,
|
|
int CareActivityId,
|
|
int CareActionId,
|
|
string Action,
|
|
string? DueDate,
|
|
string Due,
|
|
string Status)
|
|
{
|
|
public static CareTaskDto FromAssignment(
|
|
PlantCareScheduleAssignment assignment,
|
|
DateOnly? lastPerformedOn,
|
|
int completedOccurrences,
|
|
DateOnly? snoozedUntil,
|
|
DateOnly today)
|
|
{
|
|
var schedule = assignment.PlantCareSchedule;
|
|
var nextCare = PlantCareFormatter.GetNextCareDate(schedule, lastPerformedOn, completedOccurrences, snoozedUntil);
|
|
|
|
return new CareTaskDto(
|
|
schedule.Id,
|
|
assignment.PlantId,
|
|
assignment.Plant.Nickname,
|
|
schedule.CareActivityId,
|
|
schedule.CareActionId,
|
|
schedule.CareActivity.Name,
|
|
nextCare?.ToString("yyyy-MM-dd"),
|
|
PlantCareFormatter.FormatRelativeDate(nextCare, today, "Unscheduled"),
|
|
PlantCareFormatter.GetStatus(nextCare, today));
|
|
}
|
|
}
|
|
|
|
public record PlantFlagDefinitionDto(
|
|
int Id,
|
|
string Name,
|
|
string Color)
|
|
{
|
|
public static PlantFlagDefinitionDto FromDefinition(PlantFlagDefinition definition) =>
|
|
new(
|
|
definition.Id,
|
|
definition.Name,
|
|
definition.Color);
|
|
}
|
|
|
|
public record PlantFlagDto(
|
|
int Id,
|
|
int PlantFlagDefinitionId,
|
|
string Name,
|
|
string Color,
|
|
DateOnly StartedOn,
|
|
DateOnly? ResolvedOn,
|
|
string? Notes)
|
|
{
|
|
public static PlantFlagDto FromPlantFlag(PlantFlag flag) =>
|
|
new(
|
|
flag.Id,
|
|
flag.PlantFlagDefinitionId,
|
|
flag.Definition.Name,
|
|
flag.Definition.Color,
|
|
flag.StartedOn,
|
|
flag.ResolvedOn,
|
|
flag.Notes);
|
|
}
|
|
|
|
public record ActionLogDto(
|
|
int Id,
|
|
int PlantId,
|
|
string PlantName,
|
|
int CareActivityId,
|
|
int CareActionId,
|
|
string Action,
|
|
string? Notes,
|
|
DateOnly PerformedOn,
|
|
IReadOnlyList<ActionLogResourceDto> Resources)
|
|
{
|
|
public static ActionLogDto FromActionLog(ActionLog log) =>
|
|
new(
|
|
log.Id,
|
|
log.PlantId,
|
|
log.Plant.Nickname,
|
|
log.CareActivityId,
|
|
log.CareActionId,
|
|
log.ActionNameSnapshot,
|
|
log.Notes,
|
|
log.PerformedOn,
|
|
log.Resources
|
|
.Select(resource => ActionLogResourceDto.FromActionLogResource(resource))
|
|
.ToList());
|
|
}
|
|
|
|
public record ActionLogResourceDto(
|
|
int ActionResourceId,
|
|
string Name,
|
|
decimal? Quantity,
|
|
string? Unit)
|
|
{
|
|
public static ActionLogResourceDto FromActionLogResource(ActionLogResource resource) =>
|
|
new(
|
|
resource.ActionResourceId,
|
|
resource.ActionResource.Name,
|
|
resource.Quantity,
|
|
resource.Unit);
|
|
}
|
|
|
|
public record CareHistoryEventDto(
|
|
int Id,
|
|
string Type,
|
|
int PlantId,
|
|
string PlantName,
|
|
int CareActivityId,
|
|
string Action,
|
|
DateOnly Date,
|
|
string? Notes,
|
|
IReadOnlyList<ActionLogResourceDto> Resources)
|
|
{
|
|
public static CareHistoryEventDto FromActionLog(ActionLog log) =>
|
|
new(
|
|
log.Id,
|
|
"log",
|
|
log.PlantId,
|
|
log.Plant.Nickname,
|
|
log.CareActivityId,
|
|
log.ActionNameSnapshot,
|
|
log.PerformedOn,
|
|
log.Notes,
|
|
log.Resources
|
|
.Select(ActionLogResourceDto.FromActionLogResource)
|
|
.ToList());
|
|
|
|
public static CareHistoryEventDto FromDismissal(CareDismissal dismissal) =>
|
|
new(
|
|
dismissal.Id,
|
|
"dismissal",
|
|
dismissal.PlantId,
|
|
dismissal.Plant.Nickname,
|
|
dismissal.CareActivityId,
|
|
dismissal.CareActivity.Name,
|
|
dismissal.DismissedOn,
|
|
dismissal.Notes,
|
|
[]);
|
|
|
|
public static CareHistoryEventDto FromSnooze(CareSnooze snooze) =>
|
|
new(
|
|
snooze.Id,
|
|
"snooze",
|
|
snooze.PlantId,
|
|
snooze.Plant.Nickname,
|
|
snooze.CareActivityId,
|
|
snooze.CareActivity.Name,
|
|
snooze.SnoozedUntil,
|
|
snooze.Notes,
|
|
[]);
|
|
}
|
|
|
|
internal static class PlantCareFormatter
|
|
{
|
|
public static DateOnly? GetNextCareDate(
|
|
PlantCareSchedule schedule,
|
|
DateOnly? lastPerformedOn,
|
|
int completedOccurrences = 0,
|
|
DateOnly? snoozedUntil = null)
|
|
{
|
|
if (schedule.EndsMode == "after" && schedule.EndsAfterOccurrences is not null && completedOccurrences >= schedule.EndsAfterOccurrences)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
DateOnly? nextCare;
|
|
if (schedule.RecurrenceMode == "none")
|
|
{
|
|
nextCare = schedule.ScheduledFor is not null && (lastPerformedOn is null || schedule.ScheduledFor > lastPerformedOn)
|
|
? schedule.ScheduledFor
|
|
: null;
|
|
}
|
|
else if (lastPerformedOn is null)
|
|
{
|
|
nextCare = schedule.ScheduledFor;
|
|
}
|
|
else
|
|
{
|
|
nextCare = AddInterval(lastPerformedOn.Value, schedule);
|
|
}
|
|
|
|
if (nextCare is not null && snoozedUntil is not null && snoozedUntil > nextCare)
|
|
{
|
|
nextCare = snoozedUntil;
|
|
}
|
|
|
|
if (nextCare is not null && schedule.EndsMode == "on" && schedule.EndsOn is not null && nextCare > schedule.EndsOn)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return nextCare;
|
|
}
|
|
|
|
private static DateOnly AddInterval(DateOnly date, PlantCareSchedule schedule)
|
|
{
|
|
var repeatEvery = Math.Clamp(schedule.RepeatEvery, 1, 365);
|
|
var unit = schedule.RecurrenceMode == "custom" ? schedule.RepeatUnit : schedule.RecurrenceMode;
|
|
|
|
return unit switch
|
|
{
|
|
"day" or "daily" => date.AddDays(repeatEvery),
|
|
"week" when !string.IsNullOrWhiteSpace(schedule.RepeatOnDays) => GetNextSelectedWeekday(date, schedule.RepeatOnDays, repeatEvery),
|
|
"week" or "weekly" => date.AddDays(repeatEvery * 7),
|
|
"month" or "monthly" => date.AddMonths(repeatEvery),
|
|
"year" or "yearly" => date.AddYears(repeatEvery),
|
|
_ => date.AddDays(Math.Clamp(schedule.EveryDays, 1, 365))
|
|
};
|
|
}
|
|
|
|
private static DateOnly GetNextSelectedWeekday(DateOnly date, string repeatOnDays, int repeatEvery)
|
|
{
|
|
var selectedDays = repeatOnDays
|
|
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
|
.Select(ParseDayOfWeek)
|
|
.OfType<DayOfWeek>()
|
|
.ToHashSet();
|
|
if (selectedDays.Count == 0)
|
|
{
|
|
return date.AddDays(repeatEvery * 7);
|
|
}
|
|
|
|
var maxDays = Math.Max(7, repeatEvery * 7);
|
|
for (var offset = 1; offset <= maxDays; offset++)
|
|
{
|
|
var candidate = date.AddDays(offset);
|
|
if (selectedDays.Contains(candidate.DayOfWeek))
|
|
{
|
|
return candidate;
|
|
}
|
|
}
|
|
|
|
return date.AddDays(repeatEvery * 7);
|
|
}
|
|
|
|
private static DayOfWeek? ParseDayOfWeek(string value) =>
|
|
value.ToUpperInvariant() switch
|
|
{
|
|
"SU" => DayOfWeek.Sunday,
|
|
"MO" => DayOfWeek.Monday,
|
|
"TU" => DayOfWeek.Tuesday,
|
|
"WE" => DayOfWeek.Wednesday,
|
|
"TH" => DayOfWeek.Thursday,
|
|
"FR" => DayOfWeek.Friday,
|
|
"SA" => DayOfWeek.Saturday,
|
|
_ => null
|
|
};
|
|
|
|
public static string GetStatus(DateOnly? date, DateOnly today)
|
|
{
|
|
if (date is null)
|
|
{
|
|
return "unscheduled";
|
|
}
|
|
|
|
if (date <= today)
|
|
{
|
|
return "due";
|
|
}
|
|
|
|
return date <= today.AddDays(2) ? "soon" : "ok";
|
|
}
|
|
|
|
public static string FormatRelativeDate(DateOnly? date, DateOnly today, string fallback)
|
|
{
|
|
if (date is null)
|
|
{
|
|
return fallback;
|
|
}
|
|
|
|
var days = date.Value.DayNumber - today.DayNumber;
|
|
|
|
return days switch
|
|
{
|
|
0 => "Today",
|
|
1 => "Tomorrow",
|
|
-1 => "Yesterday",
|
|
> 1 => $"In {days} days",
|
|
< -1 => $"{Math.Abs(days)} days ago",
|
|
};
|
|
}
|
|
}
|
|
}
|