460 lines
17 KiB
C#
460 lines
17 KiB
C#
using ClosedXML.Excel;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using plant_manager.Data;
|
|
|
|
namespace plant_manager.Endpoints
|
|
{
|
|
public static class ExportEndpoints
|
|
{
|
|
private const string SpreadsheetContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
|
|
|
public static void MapExportEndpoints(this WebApplication app)
|
|
{
|
|
app.MapGet("/api/export/spreadsheet", async (ApplicationDbContext db) =>
|
|
{
|
|
using var workbook = new XLWorkbook();
|
|
|
|
await AddPlantsSheet(workbook, db);
|
|
await AddCareSchedulesSheet(workbook, db);
|
|
await AddActionLogsSheet(workbook, db);
|
|
await AddFlagsSheet(workbook, db);
|
|
await AddFlagDefinitionsSheet(workbook, db);
|
|
await AddGroupsSheet(workbook, db);
|
|
await AddTaxaSheet(workbook, db);
|
|
await AddLocationsSheet(workbook, db);
|
|
await AddActivitiesSheet(workbook, db);
|
|
await AddResourcesSheet(workbook, db);
|
|
await AddRecipesSheet(workbook, db);
|
|
|
|
using var stream = new MemoryStream();
|
|
workbook.SaveAs(stream);
|
|
|
|
var fileName = $"plant-man-export-{DateTime.UtcNow:yyyyMMdd-HHmm}.xlsx";
|
|
return Results.File(stream.ToArray(), SpreadsheetContentType, fileName);
|
|
});
|
|
}
|
|
|
|
private static async Task AddPlantsSheet(XLWorkbook workbook, ApplicationDbContext db)
|
|
{
|
|
var plants = await db.Plants
|
|
.Include(plant => plant.Taxon)
|
|
.Include(plant => plant.Location)
|
|
.Include(plant => plant.Flags)
|
|
.ThenInclude(flag => flag.Definition)
|
|
.Include(plant => plant.GroupMemberships)
|
|
.ThenInclude(membership => membership.PlantGroup)
|
|
.Include(plant => plant.CareScheduleAssignments)
|
|
.ThenInclude(assignment => assignment.PlantCareSchedule)
|
|
.ThenInclude(schedule => schedule.CareActivity)
|
|
.Include(plant => plant.ActionLogs)
|
|
.AsSplitQuery()
|
|
.OrderBy(plant => plant.Nickname)
|
|
.ToListAsync();
|
|
var rows = plants
|
|
.Select(plant => new object?[]
|
|
{
|
|
plant.Id,
|
|
plant.Nickname,
|
|
FormatDate(plant.Birthday),
|
|
plant.Taxon == null ? "" : plant.Taxon.Name,
|
|
plant.Taxon == null ? "" : $"{plant.Taxon.Genus} {plant.Taxon.Species}",
|
|
plant.Location == null ? "" : plant.Location.Name,
|
|
string.Join(", ", plant.GroupMemberships
|
|
.OrderBy(membership => membership.PlantGroup.Name)
|
|
.Select(membership => membership.PlantGroup.Name)),
|
|
string.Join(", ", plant.Flags
|
|
.Where(flag => flag.ResolvedOn == null)
|
|
.OrderBy(flag => flag.Definition.Name)
|
|
.Select(flag => flag.Definition.Name)),
|
|
plant.CareScheduleAssignments.Count,
|
|
plant.ActionLogs.Count
|
|
})
|
|
.ToList();
|
|
|
|
AddSheet(workbook, "Plants", [
|
|
"ID",
|
|
"Name",
|
|
"Birthday",
|
|
"Taxon",
|
|
"Scientific Name",
|
|
"Location",
|
|
"Groups",
|
|
"Active Flags",
|
|
"Care Schedules",
|
|
"Action Logs"
|
|
], rows);
|
|
}
|
|
|
|
private static async Task AddCareSchedulesSheet(XLWorkbook workbook, ApplicationDbContext db)
|
|
{
|
|
var schedules = await db.PlantCareSchedules
|
|
.Include(schedule => schedule.CareActivity)
|
|
.Include(schedule => schedule.CareAction)
|
|
.Include(schedule => schedule.Assignments)
|
|
.ThenInclude(assignment => assignment.Plant)
|
|
.OrderBy(schedule => schedule.CareActivity.Name)
|
|
.ThenBy(schedule => schedule.Id)
|
|
.ToListAsync();
|
|
var rows = schedules
|
|
.Select(schedule => new object?[]
|
|
{
|
|
schedule.Id,
|
|
string.Join(", ", schedule.Assignments
|
|
.OrderBy(assignment => assignment.Plant.Nickname)
|
|
.Select(assignment => assignment.PlantId)),
|
|
string.Join(", ", schedule.Assignments
|
|
.OrderBy(assignment => assignment.Plant.Nickname)
|
|
.Select(assignment => assignment.Plant.Nickname)),
|
|
schedule.CareActivity.Name,
|
|
schedule.CareAction.Name,
|
|
schedule.EveryDays,
|
|
FormatDate(schedule.ScheduledFor),
|
|
schedule.RecurrenceMode,
|
|
schedule.RepeatEvery,
|
|
schedule.RepeatUnit,
|
|
schedule.RepeatOnDays ?? "",
|
|
schedule.EndsMode,
|
|
FormatDate(schedule.EndsOn),
|
|
schedule.EndsAfterOccurrences
|
|
})
|
|
.ToList();
|
|
|
|
AddSheet(workbook, "Care Schedules", [
|
|
"ID",
|
|
"Plant IDs",
|
|
"Plants",
|
|
"Activity",
|
|
"Primary Action",
|
|
"Every Days",
|
|
"Scheduled For",
|
|
"Recurrence Mode",
|
|
"Repeat Every",
|
|
"Repeat Unit",
|
|
"Repeat On Days",
|
|
"Ends Mode",
|
|
"Ends On",
|
|
"Ends After Occurrences"
|
|
], rows);
|
|
}
|
|
|
|
private static async Task AddActionLogsSheet(XLWorkbook workbook, ApplicationDbContext db)
|
|
{
|
|
var logs = await db.ActionLogs
|
|
.Include(log => log.Plant)
|
|
.Include(log => log.CareActivity)
|
|
.Include(log => log.CareAction)
|
|
.Include(log => log.Resources)
|
|
.ThenInclude(resource => resource.ActionResource)
|
|
.OrderByDescending(log => log.PerformedOn)
|
|
.ThenBy(log => log.Plant.Nickname)
|
|
.ToListAsync();
|
|
var rows = logs
|
|
.Select(log => new object?[]
|
|
{
|
|
log.Id,
|
|
log.PlantId,
|
|
log.Plant.Nickname,
|
|
FormatDate(log.PerformedOn),
|
|
log.CareActivity.Name,
|
|
log.CareAction.Name,
|
|
log.ActionNameSnapshot,
|
|
string.Join(", ", log.Resources
|
|
.OrderBy(resource => resource.ActionResource.Name)
|
|
.Select(resource => FormatQuantity(resource.ActionResource.Name, resource.Quantity, resource.Unit))),
|
|
log.Notes ?? ""
|
|
})
|
|
.ToList();
|
|
|
|
AddSheet(workbook, "Action Logs", [
|
|
"ID",
|
|
"Plant ID",
|
|
"Plant",
|
|
"Performed On",
|
|
"Activity",
|
|
"Primary Action",
|
|
"Action Snapshot",
|
|
"Resources",
|
|
"Notes"
|
|
], rows);
|
|
}
|
|
|
|
private static async Task AddFlagsSheet(XLWorkbook workbook, ApplicationDbContext db)
|
|
{
|
|
var flags = await db.PlantFlags
|
|
.Include(flag => flag.Plant)
|
|
.Include(flag => flag.Definition)
|
|
.OrderBy(flag => flag.ResolvedOn != null)
|
|
.ThenBy(flag => flag.Plant.Nickname)
|
|
.ThenBy(flag => flag.Definition.Name)
|
|
.ToListAsync();
|
|
var rows = flags
|
|
.Select(flag => new object?[]
|
|
{
|
|
flag.Id,
|
|
flag.PlantId,
|
|
flag.Plant.Nickname,
|
|
flag.Definition.Name,
|
|
flag.Definition.Color,
|
|
FormatDate(flag.StartedOn),
|
|
FormatDate(flag.ResolvedOn),
|
|
flag.Notes ?? ""
|
|
})
|
|
.ToList();
|
|
|
|
AddSheet(workbook, "Flags", [
|
|
"ID",
|
|
"Plant ID",
|
|
"Plant",
|
|
"Flag",
|
|
"Color",
|
|
"Started On",
|
|
"Resolved On",
|
|
"Notes"
|
|
], rows);
|
|
}
|
|
|
|
private static async Task AddFlagDefinitionsSheet(XLWorkbook workbook, ApplicationDbContext db)
|
|
{
|
|
var rows = await db.PlantFlagDefinitions
|
|
.OrderBy(definition => definition.Name)
|
|
.Select(definition => new object?[]
|
|
{
|
|
definition.Id,
|
|
definition.Name,
|
|
definition.Color
|
|
})
|
|
.ToListAsync();
|
|
|
|
AddSheet(workbook, "Flag Definitions", [
|
|
"ID",
|
|
"Name",
|
|
"Color"
|
|
], rows);
|
|
}
|
|
|
|
private static async Task AddGroupsSheet(XLWorkbook workbook, ApplicationDbContext db)
|
|
{
|
|
var groups = await db.PlantGroups
|
|
.Include(group => group.Memberships)
|
|
.ThenInclude(membership => membership.Plant)
|
|
.OrderBy(group => group.Name)
|
|
.ToListAsync();
|
|
var rows = groups
|
|
.Select(group => new object?[]
|
|
{
|
|
group.Id,
|
|
group.Name,
|
|
group.Memberships.Count,
|
|
string.Join(", ", group.Memberships
|
|
.OrderBy(membership => membership.Plant.Nickname)
|
|
.Select(membership => membership.Plant.Nickname)),
|
|
group.Notes ?? ""
|
|
})
|
|
.ToList();
|
|
|
|
AddSheet(workbook, "Groups", [
|
|
"ID",
|
|
"Name",
|
|
"Plant Count",
|
|
"Plants",
|
|
"Notes"
|
|
], rows);
|
|
}
|
|
|
|
private static async Task AddTaxaSheet(XLWorkbook workbook, ApplicationDbContext db)
|
|
{
|
|
var rows = await db.PlantTaxa
|
|
.OrderBy(taxon => taxon.Name)
|
|
.Select(taxon => new object?[]
|
|
{
|
|
taxon.Id,
|
|
taxon.Name,
|
|
taxon.Genus,
|
|
taxon.Species,
|
|
taxon.Cultivar ?? "",
|
|
taxon.Variety ?? "",
|
|
taxon.Authority ?? "",
|
|
taxon.Family ?? "",
|
|
taxon.CommonName ?? "",
|
|
taxon.ExternalSource ?? "",
|
|
taxon.ExternalId ?? ""
|
|
})
|
|
.ToListAsync();
|
|
|
|
AddSheet(workbook, "Taxa", [
|
|
"ID",
|
|
"Name",
|
|
"Genus",
|
|
"Species",
|
|
"Cultivar",
|
|
"Variety",
|
|
"Authority",
|
|
"Family",
|
|
"Common Name",
|
|
"External Source",
|
|
"External ID"
|
|
], rows);
|
|
}
|
|
|
|
private static async Task AddLocationsSheet(XLWorkbook workbook, ApplicationDbContext db)
|
|
{
|
|
var locations = await db.PlantLocations
|
|
.Include(location => location.Plants)
|
|
.OrderBy(location => location.Name)
|
|
.ToListAsync();
|
|
var rows = locations
|
|
.Select(location => new object?[]
|
|
{
|
|
location.Id,
|
|
location.Name,
|
|
location.Plants.Count,
|
|
location.Notes ?? ""
|
|
})
|
|
.ToList();
|
|
|
|
AddSheet(workbook, "Locations", [
|
|
"ID",
|
|
"Name",
|
|
"Plant Count",
|
|
"Notes"
|
|
], rows);
|
|
}
|
|
|
|
private static async Task AddActivitiesSheet(XLWorkbook workbook, ApplicationDbContext db)
|
|
{
|
|
var activities = await db.CareActivities
|
|
.Include(activity => activity.Actions)
|
|
.ThenInclude(action => action.CareAction)
|
|
.Include(activity => activity.Actions)
|
|
.ThenInclude(action => action.Resources)
|
|
.ThenInclude(resource => resource.ActionResource)
|
|
.AsSplitQuery()
|
|
.OrderBy(activity => activity.Name)
|
|
.ToListAsync();
|
|
var rows = activities
|
|
.Select(activity => new object?[]
|
|
{
|
|
activity.Id,
|
|
activity.Name,
|
|
string.Join(", ", activity.Actions
|
|
.OrderBy(action => action.SortOrder)
|
|
.Select(action => action.CareAction.Name)),
|
|
string.Join(", ", activity.Actions
|
|
.SelectMany(action => action.Resources)
|
|
.OrderBy(resource => resource.ActionResource.Name)
|
|
.Select(resource => FormatQuantity(resource.ActionResource.Name, resource.Quantity, resource.Unit))),
|
|
activity.Notes ?? ""
|
|
})
|
|
.ToList();
|
|
|
|
AddSheet(workbook, "Activities", [
|
|
"ID",
|
|
"Name",
|
|
"Actions",
|
|
"Resources",
|
|
"Notes"
|
|
], rows);
|
|
}
|
|
|
|
private static async Task AddResourcesSheet(XLWorkbook workbook, ApplicationDbContext db)
|
|
{
|
|
var rows = await db.ActionResources
|
|
.Include(resource => resource.ProducedByRecipe)
|
|
.OrderBy(resource => resource.Name)
|
|
.Select(resource => new object?[]
|
|
{
|
|
resource.Id,
|
|
resource.Name,
|
|
resource.ProducedByRecipe == null ? "" : resource.ProducedByRecipe.Name,
|
|
resource.Notes ?? ""
|
|
})
|
|
.ToListAsync();
|
|
|
|
AddSheet(workbook, "Resources", [
|
|
"ID",
|
|
"Name",
|
|
"Produced By Recipe",
|
|
"Notes"
|
|
], rows);
|
|
}
|
|
|
|
private static async Task AddRecipesSheet(XLWorkbook workbook, ApplicationDbContext db)
|
|
{
|
|
var recipes = await db.Recipes
|
|
.Include(recipe => recipe.OutputResource)
|
|
.Include(recipe => recipe.Components)
|
|
.ThenInclude(component => component.ActionResource)
|
|
.OrderBy(recipe => recipe.Name)
|
|
.ToListAsync();
|
|
var rows = recipes
|
|
.Select(recipe => new object?[]
|
|
{
|
|
recipe.Id,
|
|
recipe.Name,
|
|
recipe.MeasurementMode,
|
|
recipe.OutputResource == null ? "" : recipe.OutputResource.Name,
|
|
string.Join(", ", recipe.Components
|
|
.OrderBy(component => component.SortOrder)
|
|
.Select(component => FormatQuantity(component.ActionResource.Name, component.Quantity, component.Unit))),
|
|
recipe.Notes ?? ""
|
|
})
|
|
.ToList();
|
|
|
|
AddSheet(workbook, "Recipes", [
|
|
"ID",
|
|
"Name",
|
|
"Measurement Mode",
|
|
"Output Resource",
|
|
"Components",
|
|
"Notes"
|
|
], rows);
|
|
}
|
|
|
|
private static void AddSheet(
|
|
XLWorkbook workbook,
|
|
string name,
|
|
IReadOnlyList<string> headers,
|
|
IReadOnlyList<object?[]> rows)
|
|
{
|
|
var worksheet = workbook.Worksheets.Add(name);
|
|
for (var column = 0; column < headers.Count; column++)
|
|
{
|
|
worksheet.Cell(1, column + 1).Value = headers[column];
|
|
}
|
|
|
|
for (var row = 0; row < rows.Count; row++)
|
|
{
|
|
for (var column = 0; column < headers.Count; column++)
|
|
{
|
|
var value = column < rows[row].Length ? rows[row][column] : null;
|
|
worksheet.Cell(row + 2, column + 1).Value = XLCellValue.FromObject(value ?? "");
|
|
}
|
|
}
|
|
|
|
var usedRange = worksheet.Range(1, 1, Math.Max(rows.Count + 1, 1), headers.Count);
|
|
usedRange.SetAutoFilter();
|
|
worksheet.SheetView.FreezeRows(1);
|
|
|
|
var headerRange = worksheet.Range(1, 1, 1, headers.Count);
|
|
headerRange.Style.Font.Bold = true;
|
|
headerRange.Style.Fill.BackgroundColor = XLColor.FromHtml("#DBE7D2");
|
|
|
|
worksheet.Columns().AdjustToContents();
|
|
}
|
|
|
|
private static string FormatDate(DateOnly? date) =>
|
|
date?.ToString("yyyy-MM-dd") ?? "";
|
|
|
|
private static string FormatQuantity(string name, decimal? quantity, string? unit)
|
|
{
|
|
if (quantity is null)
|
|
{
|
|
return name;
|
|
}
|
|
|
|
var suffix = string.IsNullOrWhiteSpace(unit) ? "" : $" {unit}";
|
|
return $"{name} ({quantity:0.##}{suffix})";
|
|
}
|
|
}
|
|
}
|