Implemented the floor-level ordering pass using the existing integer SortOrder as the user-facing Level.

This commit is contained in:
Nick Beckley 2026-06-21 20:49:17 +01:00
parent 11b29b2504
commit 9fc4374df3
4 changed files with 79 additions and 9 deletions

View File

@ -7563,6 +7563,7 @@ public sealed class FloorPlanService(
}
var projectLocations = await locations.ListByProjectAsync(project.ProjectID);
var orderedFloors = data.Floors.OrderBy(x => x.SortOrder).ThenBy(x => x.Name).ToList();
return new FloorPlanEditorViewModel
{
Project = project,
@ -7576,27 +7577,27 @@ public sealed class FloorPlanService(
Name = data.FloorPlan.Name,
Description = data.FloorPlan.Description
},
Floors = data.Floors.Select(ToFloorEdit).ToList(),
Floors = orderedFloors.Select(ToFloorEdit).ToList(),
Blocks = data.Blocks.Select(ToBlockEdit).ToList(),
Transitions = data.Transitions.Select(ToTransitionEdit).ToList(),
NewFloor = new FloorPlanFloorEditViewModel
{
FloorPlanID = data.FloorPlan.FloorPlanID,
Name = "New floor",
SortOrder = data.Floors.Any() ? data.Floors.Max(x => x.SortOrder) + 10 : 10,
SortOrder = orderedFloors.Any() ? orderedFloors.Max(x => x.SortOrder) + 1 : 0,
GridWidth = 24,
GridHeight = 16
},
NewBlock = new FloorPlanBlockEditViewModel
{
FloorPlanFloorID = data.Floors.FirstOrDefault()?.FloorPlanFloorID ?? 0,
FloorPlanFloorID = orderedFloors.FirstOrDefault()?.FloorPlanFloorID ?? 0,
BlockType = FloorPlanBlockTypes.Room,
WidthCells = 2,
HeightCells = 1
},
NewTransition = new FloorPlanTransitionEditViewModel
{
FloorPlanFloorID = data.Floors.FirstOrDefault()?.FloorPlanFloorID ?? 0,
FloorPlanFloorID = orderedFloors.FirstOrDefault()?.FloorPlanFloorID ?? 0,
TransitionType = FloorPlanTransitionTypes.Door
},
LocationOptions = ToOptionalLocationOptions(projectLocations, "Create/link automatically"),
@ -7627,7 +7628,7 @@ public sealed class FloorPlanService(
FloorPlanID = floorPlanId,
LocationID = floorLocationId,
Name = "Ground Floor",
SortOrder = 10,
SortOrder = 0,
GridWidth = 24,
GridHeight = 16
});
@ -8005,6 +8006,11 @@ public sealed class FloorPlanService(
private static void ValidateFloor(FloorPlanFloorEditViewModel model)
{
if (model.SortOrder is < -10 or > 50)
{
throw new InvalidOperationException("Floor level must be between -10 and 50.");
}
if (model.GridWidth is < 8 or > 80 || model.GridHeight is < 8 or > 80)
{
throw new InvalidOperationException("Grid width and height must be between 8 and 80.");

View File

@ -0,0 +1,31 @@
-- Reinterpret the existing FloorPlanFloors.SortOrder integer as user-facing floor Level.
-- This preserves current relative order for legacy 10-step SortOrder data without adding schema.
IF OBJECT_ID(N'dbo.FloorPlanFloors', N'U') IS NOT NULL
BEGIN
;WITH LegacyPlans AS
(
SELECT FloorPlanID
FROM dbo.FloorPlanFloors
GROUP BY FloorPlanID
HAVING COUNT(*) > 0
AND MIN(SortOrder) >= 10
AND SUM(CASE WHEN SortOrder % 10 = 0 THEN 0 ELSE 1 END) = 0
),
OrderedFloors AS
(
SELECT
fpf.FloorPlanFloorID,
ROW_NUMBER() OVER (PARTITION BY fpf.FloorPlanID ORDER BY fpf.SortOrder, fpf.Name, fpf.FloorPlanFloorID) - 1 AS FloorLevel
FROM dbo.FloorPlanFloors fpf
INNER JOIN LegacyPlans lp ON lp.FloorPlanID = fpf.FloorPlanID
)
UPDATE fpf
SET
SortOrder = ordered.FloorLevel,
ModifiedDate = SYSUTCDATETIME()
FROM dbo.FloorPlanFloors fpf
INNER JOIN OrderedFloors ordered ON ordered.FloorPlanFloorID = fpf.FloorPlanFloorID
WHERE fpf.SortOrder <> ordered.FloorLevel;
END;
GO

View File

@ -991,6 +991,8 @@ public sealed class TimelineMetricSettingOptionViewModel
public int MetricTypeID { get; set; }
public string MetricName { get; set; } = string.Empty;
public string? Description { get; set; }
[Display(Name = "Level")]
[Range(-10, 50)]
public int SortOrder { get; set; }
public bool IsSelected { get; set; }
}

View File

@ -113,12 +113,15 @@
<div class="floor-plan-tool-card mt-3">
<h3>Add floor</h3>
<input asp-for="NewFloor.FloorPlanID" type="hidden" name="FloorPlanID" form="floor-plan-add-floor-form" />
<input asp-for="NewFloor.SortOrder" type="hidden" name="SortOrder" form="floor-plan-add-floor-form" />
<div class="row g-2 align-items-end">
<div class="col-md-4">
<label class="form-label">New floor</label>
<input asp-for="NewFloor.Name" class="form-control form-control-sm" name="Name" form="floor-plan-add-floor-form" />
</div>
<div class="col-md-2">
<label class="form-label">Level</label>
<input asp-for="NewFloor.SortOrder" class="form-control form-control-sm" name="SortOrder" min="-10" max="50" step="1" form="floor-plan-add-floor-form" />
</div>
<div class="col-md-2">
<label class="form-label">Width</label>
<input asp-for="NewFloor.GridWidth" class="form-control form-control-sm" name="GridWidth" min="8" max="80" form="floor-plan-add-floor-form" />
@ -131,6 +134,7 @@
<button class="btn btn-outline-primary btn-sm w-100" type="submit" form="floor-plan-add-floor-form">Add floor</button>
</div>
</div>
<p class="form-text mb-0">Used to order floors and later to connect stairs up/down.</p>
</div>
</section>
@ -564,11 +568,15 @@
<div class="floor-plan-tool-subsection">
<h3>Add floor</h3>
<input asp-for="NewFloor.FloorPlanID" type="hidden" name="FloorPlanID" form="floor-plan-add-floor-form" />
<input asp-for="NewFloor.SortOrder" type="hidden" name="SortOrder" form="floor-plan-add-floor-form" />
<div class="mb-2">
<label class="form-label">New floor</label>
<input asp-for="NewFloor.Name" class="form-control form-control-sm" name="Name" form="floor-plan-add-floor-form" />
</div>
<div class="mb-2">
<label class="form-label">Level</label>
<input asp-for="NewFloor.SortOrder" class="form-control form-control-sm" name="SortOrder" min="-10" max="50" step="1" form="floor-plan-add-floor-form" />
<div class="form-text">Used to order floors and later to connect stairs up/down.</div>
</div>
<div class="row g-2">
<div class="col-6">
<label class="form-label">Width</label>
@ -590,12 +598,16 @@
<div class="floor-plan-floor-editor @(isActive ? string.Empty : "d-none")" data-floor-editor="@floor.FloorPlanFloorID">
<input asp-for="Floors[i].FloorPlanFloorID" type="hidden" />
<input asp-for="Floors[i].FloorPlanID" type="hidden" />
<input asp-for="Floors[i].SortOrder" type="hidden" />
<input asp-for="Floors[i].LocationID" type="hidden" />
<div class="mb-2">
<label class="form-label">Floor name</label>
<input asp-for="Floors[i].Name" class="form-control form-control-sm" />
</div>
<div class="mb-2">
<label class="form-label">Level</label>
<input asp-for="Floors[i].SortOrder" class="form-control form-control-sm" min="-10" max="50" step="1" data-floor-level="@floor.FloorPlanFloorID" />
<div class="form-text">Used to order floors and later to connect stairs up/down.</div>
</div>
<p class="muted">Location: @(string.IsNullOrWhiteSpace(floor.LocationPath) ? "A child Location will be created or linked on save." : floor.LocationPath)</p>
<div class="row g-2">
<div class="col-6">
@ -907,6 +919,7 @@
let saveTimer = null;
let saveInFlight = false;
let saveAgain = false;
let refreshAfterSave = false;
const backgroundTimers = new Map();
function setToolboxTab(tabName) {
@ -1068,6 +1081,10 @@
}
setSaveStatus("saved");
if (refreshAfterSave) {
refreshAfterSave = false;
await refreshWorkspaceAsync(workspaceState());
}
} catch (error) {
setSaveStatus("failed", error.message || "Save failed");
} finally {
@ -1831,11 +1848,25 @@
});
});
editor.querySelectorAll("[data-floor-level]").forEach(input => {
const updateFloorLevel = (clampValue = false) => {
const parsed = Number(input.value);
if (!Number.isFinite(parsed)) return;
if (clampValue && Number.isFinite(parsed)) {
input.value = Math.max(-10, Math.min(parsed, 50));
}
refreshAfterSave = true;
markDirty();
};
input.addEventListener("input", () => updateFloorLevel());
input.addEventListener("change", () => updateFloorLevel(true));
});
editor.querySelectorAll("input, select, textarea").forEach(input => {
if (input.matches("[form='floor-plan-add-block-form'], [form='floor-plan-add-floor-form']")) return;
if (input.matches("[form^='floor-plan-background-'], [form^='floor-plan-transition-add-']")) return;
if (input.matches("[data-background-field]")) return;
if (input.matches("[data-floor-width],[data-floor-height]")) return;
if (input.matches("[data-floor-width],[data-floor-height],[data-floor-level]")) return;
if (input.matches("[data-block-field='x'],[data-block-field='y'],[data-block-field='w'],[data-block-field='h'],[data-block-field='type'],[data-block-field='location']")) return;
const eventName = input.tagName === "TEXTAREA" || input.type === "text" ? "input" : "change";
input.addEventListener(eventName, markDirty);