Implemented the cleanup/refinement pass.
What changed: Removed the standalone floor-plan details panel under the page title. Added current-floor Description and editable Root Location into the Floors toolbox tab. Added live computed Occupancy Warnings in Occupancy mode. Preserved the existing AJAX workspace flow, including active floor/toolbox tab/mode/selected occupancy scene through refreshes. Added floor description support via a new migration: [081_FloorPlanFloorDescriptionAndOccupancyWarnings.sql](C:/Source/PlotLine/PlotLine/Sql/081_FloorPlanFloorDescriptionAndOccupancyWarnings.sql) Database changes: Added nullable FloorPlanFloors.Description. Updated active stored procedures:FloorPlan_GetEditor FloorPlanFloor_Get FloorPlanFloor_ListByPlan FloorPlanFloor_Save Warnings are not persisted. No warning tables were added. Files changed include: [Edit.cshtml](C:/Source/PlotLine/PlotLine/Views/FloorPlans/Edit.cshtml) [CoreServices.cs](C:/Source/PlotLine/PlotLine/Services/CoreServices.cs) [Repositories.cs](C:/Source/PlotLine/PlotLine/Data/Repositories.cs) [CoreModels.cs](C:/Source/PlotLine/PlotLine/Models/CoreModels.cs) [CoreViewModels.cs](C:/Source/PlotLine/PlotLine/ViewModels/CoreViewModels.cs) [site.css](C:/Source/PlotLine/PlotLine/wwwroot/css/site.css) [site.min.css](C:/Source/PlotLine/PlotLine/wwwroot/css/site.min.css) Verification: Applied the new migration locally. Confirmed FloorPlanFloors.Description exists. Confirmed scene/occupancy source data exists for The Calendar House. dotnet build PlotLine.slnx passes with 0 warnings and 0 errors. Manual QA still needed in the browser: edit floor description/root location, switch Structure/Occupancy mode, step scenes, and confirm warnings/tokens update visually without jumps.
This commit is contained in:
parent
a943ac3da4
commit
7ad993aae3
@ -633,7 +633,7 @@ public sealed class FloorPlanRepository(ISqlConnectionFactory connectionFactory)
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleAsync<int>(
|
||||
"dbo.FloorPlanFloor_Save",
|
||||
new { floor.FloorPlanFloorID, floor.FloorPlanID, floor.LocationID, floor.Name, floor.SortOrder, floor.GridWidth, floor.GridHeight },
|
||||
new { floor.FloorPlanFloorID, floor.FloorPlanID, floor.LocationID, floor.Name, floor.Description, floor.SortOrder, floor.GridWidth, floor.GridHeight },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
|
||||
@ -1167,6 +1167,7 @@ public sealed class FloorPlanFloor
|
||||
public string? LocationName { get; set; }
|
||||
public string? LocationPath { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public int GridWidth { get; set; } = 24;
|
||||
public int GridHeight { get; set; } = 16;
|
||||
|
||||
@ -7733,6 +7733,8 @@ public sealed class FloorPlanService(
|
||||
IProjectRepository projects,
|
||||
ISceneRepository scenes,
|
||||
ISceneFloorPlanOccupancyRepository sceneOccupancy,
|
||||
ICharacterRepository characters,
|
||||
IAssetRepository assets,
|
||||
IFloorPlanRepository floorPlans,
|
||||
ILocationRepository locations,
|
||||
IFloorPlanBackgroundImageService backgroundImages,
|
||||
@ -7860,10 +7862,132 @@ public sealed class FloorPlanService(
|
||||
})
|
||||
.Where(x => x is not null)
|
||||
.Cast<FloorPlanOccupancyTokenViewModel>()
|
||||
.ToList()
|
||||
.ToList(),
|
||||
OccupancyWarnings = await BuildFloorPlanOccupancyWarningsAsync(occupancyScenes, occupancyRows, data.Blocks, data.Transitions)
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<FloorPlanOccupancyWarningViewModel>> BuildFloorPlanOccupancyWarningsAsync(
|
||||
IReadOnlyList<Scene> occupancyScenes,
|
||||
IReadOnlyList<SceneFloorPlanOccupancy> occupancyRows,
|
||||
IReadOnlyList<FloorPlanBlock> blocks,
|
||||
IReadOnlyList<FloorPlanTransition> transitions)
|
||||
{
|
||||
var warnings = new List<FloorPlanOccupancyWarningViewModel>();
|
||||
var blocksByLocation = blocks.GroupBy(x => x.LocationID).ToDictionary(x => x.Key, x => x.ToList());
|
||||
var floors = blocks.Select(x => x.FloorPlanFloorID).Distinct().ToList();
|
||||
var visibleEntranceLocationIds = transitions
|
||||
.Where(x => IsVisibleEntranceTransition(x.TransitionType))
|
||||
.SelectMany(x => new[] { x.FromLocationID, x.ToLocationID })
|
||||
.ToHashSet();
|
||||
|
||||
foreach (var scene in occupancyScenes)
|
||||
{
|
||||
var sceneRows = occupancyRows.Where(x => x.SceneID == scene.SceneID).ToList();
|
||||
var occupiedCharacterIds = sceneRows.Where(x => x.CharacterID.HasValue).Select(x => x.CharacterID!.Value).ToHashSet();
|
||||
var occupiedAssetIds = sceneRows.Where(x => x.StoryAssetID.HasValue).Select(x => x.StoryAssetID!.Value).ToHashSet();
|
||||
|
||||
var sceneCharacters = await characters.ListSceneCharactersAsync(scene.SceneID);
|
||||
foreach (var character in sceneCharacters
|
||||
.GroupBy(x => x.CharacterID)
|
||||
.Select(x => x.First())
|
||||
.Where(x => !occupiedCharacterIds.Contains(x.CharacterID)))
|
||||
{
|
||||
warnings.Add(new FloorPlanOccupancyWarningViewModel
|
||||
{
|
||||
SceneID = scene.SceneID,
|
||||
Severity = "warning",
|
||||
Message = $"{character.CharacterName} appears in this scene but has no floor-plan location."
|
||||
});
|
||||
}
|
||||
|
||||
var sceneAssets = await assets.ListEventsBySceneAsync(scene.SceneID);
|
||||
foreach (var asset in sceneAssets
|
||||
.GroupBy(x => x.StoryAssetID)
|
||||
.Select(x => x.First())
|
||||
.Where(x => !occupiedAssetIds.Contains(x.StoryAssetID)))
|
||||
{
|
||||
warnings.Add(new FloorPlanOccupancyWarningViewModel
|
||||
{
|
||||
SceneID = scene.SceneID,
|
||||
Severity = "warning",
|
||||
Message = $"{asset.AssetName} appears in this scene but has no floor-plan location."
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var row in sceneRows.Where(x => !blocksByLocation.ContainsKey(x.LocationID)))
|
||||
{
|
||||
warnings.Add(new FloorPlanOccupancyWarningViewModel
|
||||
{
|
||||
SceneID = scene.SceneID,
|
||||
Severity = "warning",
|
||||
Message = $"{OccupantName(row)} is assigned to {row.LocationName}, but that location is not placed on this floor plan."
|
||||
});
|
||||
}
|
||||
|
||||
var rowsOnPlacedLocations = sceneRows.Where(x => blocksByLocation.ContainsKey(x.LocationID)).ToList();
|
||||
var occupiedFloorIds = rowsOnPlacedLocations
|
||||
.SelectMany(x => blocksByLocation[x.LocationID].Select(block => block.FloorPlanFloorID))
|
||||
.Distinct()
|
||||
.ToHashSet();
|
||||
foreach (var floorId in floors.Where(floorId => rowsOnPlacedLocations.Any() && !occupiedFloorIds.Contains(floorId)))
|
||||
{
|
||||
warnings.Add(new FloorPlanOccupancyWarningViewModel
|
||||
{
|
||||
SceneID = scene.SceneID,
|
||||
FloorPlanFloorID = floorId,
|
||||
Severity = "info",
|
||||
Message = "Some occupants are on other floors."
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var row in rowsOnPlacedLocations.Where(x => !visibleEntranceLocationIds.Contains(x.LocationID)))
|
||||
{
|
||||
foreach (var floorId in blocksByLocation[row.LocationID].Select(x => x.FloorPlanFloorID).Distinct())
|
||||
{
|
||||
warnings.Add(new FloorPlanOccupancyWarningViewModel
|
||||
{
|
||||
SceneID = scene.SceneID,
|
||||
FloorPlanFloorID = floorId,
|
||||
Severity = "info",
|
||||
Message = $"{row.LocationName} has no visible entrance."
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return warnings
|
||||
.GroupBy(x => new { x.SceneID, x.FloorPlanFloorID, x.Severity, x.Message })
|
||||
.Select(x => x.First())
|
||||
.OrderByDescending(x => string.Equals(x.Severity, "warning", StringComparison.OrdinalIgnoreCase))
|
||||
.ThenBy(x => x.Message)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static string OccupantName(SceneFloorPlanOccupancy row)
|
||||
=> row.CharacterName ?? row.AssetName ?? "Occupant";
|
||||
|
||||
private static bool IsVisibleEntranceTransition(string? transitionType)
|
||||
{
|
||||
var normalized = (transitionType ?? string.Empty)
|
||||
.Trim()
|
||||
.Replace(" ", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("-", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||
.ToLowerInvariant();
|
||||
|
||||
return normalized is "door"
|
||||
or "doubledoor"
|
||||
or "frenchdoor"
|
||||
or "frenchdoors"
|
||||
or "slidingdoor"
|
||||
or "slidingdoors"
|
||||
or "exteriordoor"
|
||||
or "archway"
|
||||
or "openpassage"
|
||||
or "stairsup"
|
||||
or "stairsdown";
|
||||
}
|
||||
|
||||
public async Task<int> SavePlanAsync(FloorPlanEditViewModel model)
|
||||
{
|
||||
var isNew = model.FloorPlanID == 0;
|
||||
@ -7915,6 +8039,7 @@ public sealed class FloorPlanService(
|
||||
FloorPlanID = model.FloorPlanID,
|
||||
LocationID = locationId,
|
||||
Name = model.Name.Trim(),
|
||||
Description = model.Description,
|
||||
SortOrder = model.SortOrder,
|
||||
GridWidth = model.GridWidth,
|
||||
GridHeight = model.GridHeight
|
||||
@ -8497,6 +8622,7 @@ public sealed class FloorPlanService(
|
||||
LocationName = floor.LocationName,
|
||||
LocationPath = floor.LocationPath,
|
||||
Name = floor.Name,
|
||||
Description = floor.Description,
|
||||
SortOrder = floor.SortOrder,
|
||||
GridWidth = floor.GridWidth,
|
||||
GridHeight = floor.GridHeight,
|
||||
|
||||
@ -0,0 +1,238 @@
|
||||
IF COL_LENGTH(N'dbo.FloorPlanFloors', N'Description') IS NULL
|
||||
ALTER TABLE dbo.FloorPlanFloors ADD Description nvarchar(max) NULL;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.FloorPlan_GetEditor
|
||||
@FloorPlanID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
DECLARE @ProjectID int;
|
||||
SELECT @ProjectID = ProjectID
|
||||
FROM dbo.FloorPlans
|
||||
WHERE FloorPlanID = @FloorPlanID;
|
||||
|
||||
EXEC dbo.FloorPlan_Get @FloorPlanID;
|
||||
|
||||
CREATE TABLE #ProjectLocationPaths
|
||||
(
|
||||
LocationID int NOT NULL PRIMARY KEY,
|
||||
LocationName nvarchar(200) NOT NULL,
|
||||
LocationPath nvarchar(max) NOT NULL
|
||||
);
|
||||
|
||||
;WITH ProjectLocationTree AS
|
||||
(
|
||||
SELECT
|
||||
LocationID,
|
||||
LocationName,
|
||||
CAST(LocationName AS nvarchar(max)) AS LocationPath
|
||||
FROM dbo.Locations
|
||||
WHERE ProjectID = @ProjectID
|
||||
AND ParentLocationID IS NULL
|
||||
AND IsArchived = 0
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
child.LocationID,
|
||||
child.LocationName,
|
||||
CAST(parent.LocationPath + N' > ' + child.LocationName AS nvarchar(max)) AS LocationPath
|
||||
FROM dbo.Locations child
|
||||
INNER JOIN ProjectLocationTree parent ON parent.LocationID = child.ParentLocationID
|
||||
WHERE child.ProjectID = @ProjectID
|
||||
AND child.IsArchived = 0
|
||||
)
|
||||
INSERT #ProjectLocationPaths (LocationID, LocationName, LocationPath)
|
||||
SELECT LocationID, LocationName, LocationPath
|
||||
FROM ProjectLocationTree;
|
||||
|
||||
SELECT
|
||||
fpf.FloorPlanFloorID,
|
||||
fpf.FloorPlanID,
|
||||
fpf.LocationID,
|
||||
lp.LocationName,
|
||||
lp.LocationPath,
|
||||
fpf.Name,
|
||||
fpf.Description,
|
||||
fpf.SortOrder,
|
||||
fpf.GridWidth,
|
||||
fpf.GridHeight,
|
||||
fpf.BackgroundImagePath,
|
||||
fpf.BackgroundImageOriginalFileName,
|
||||
COALESCE(fpf.BackgroundOpacity, CONVERT(decimal(5,2), 0.35)) AS BackgroundOpacity,
|
||||
COALESCE(fpf.BackgroundScale, CONVERT(decimal(8,3), 1.000)) AS BackgroundScale,
|
||||
COALESCE(fpf.BackgroundOffsetX, CONVERT(decimal(10,2), 0.00)) AS BackgroundOffsetX,
|
||||
COALESCE(fpf.BackgroundOffsetY, CONVERT(decimal(10,2), 0.00)) AS BackgroundOffsetY,
|
||||
fpf.BackgroundLocked,
|
||||
fpf.CreatedDate,
|
||||
fpf.ModifiedDate
|
||||
FROM dbo.FloorPlanFloors fpf
|
||||
LEFT JOIN #ProjectLocationPaths lp ON lp.LocationID = fpf.LocationID
|
||||
WHERE fpf.FloorPlanID = @FloorPlanID
|
||||
ORDER BY fpf.SortOrder, fpf.Name;
|
||||
|
||||
SELECT
|
||||
fpb.FloorPlanBlockID,
|
||||
fpb.FloorPlanFloorID,
|
||||
fpb.LocationID,
|
||||
lp.LocationName,
|
||||
COALESCE(NULLIF(lp.LocationPath, N''), lp.LocationName) AS LocationPath,
|
||||
fpb.X,
|
||||
fpb.Y,
|
||||
fpb.WidthCells,
|
||||
fpb.HeightCells,
|
||||
fpb.BlockType,
|
||||
fpb.LabelOverride,
|
||||
fpb.Notes,
|
||||
fpb.CreatedDate,
|
||||
fpb.ModifiedDate
|
||||
FROM dbo.FloorPlanBlocks fpb
|
||||
INNER JOIN dbo.FloorPlanFloors fpf ON fpf.FloorPlanFloorID = fpb.FloorPlanFloorID
|
||||
INNER JOIN #ProjectLocationPaths lp ON lp.LocationID = fpb.LocationID
|
||||
WHERE fpf.FloorPlanID = @FloorPlanID
|
||||
ORDER BY fpf.SortOrder, fpb.Y, fpb.X, lp.LocationName;
|
||||
|
||||
SELECT
|
||||
fpt.FloorPlanTransitionID,
|
||||
fpt.FloorPlanFloorID,
|
||||
fpt.FromLocationID,
|
||||
fromPath.LocationName AS FromLocationName,
|
||||
COALESCE(NULLIF(fromPath.LocationPath, N''), fromPath.LocationName) AS FromLocationPath,
|
||||
fpt.ToLocationID,
|
||||
toPath.LocationName AS ToLocationName,
|
||||
COALESCE(NULLIF(toPath.LocationPath, N''), toPath.LocationName) AS ToLocationPath,
|
||||
fpt.TransitionType,
|
||||
fpt.Notes,
|
||||
fpt.IsLocked,
|
||||
fpt.DisplayLabel,
|
||||
fpt.PositionWithinLocation,
|
||||
fpt.CreatedDate,
|
||||
fpt.ModifiedDate
|
||||
FROM dbo.FloorPlanTransitions fpt
|
||||
INNER JOIN dbo.FloorPlanFloors fpf ON fpf.FloorPlanFloorID = fpt.FloorPlanFloorID
|
||||
INNER JOIN #ProjectLocationPaths fromPath ON fromPath.LocationID = fpt.FromLocationID
|
||||
INNER JOIN #ProjectLocationPaths toPath ON toPath.LocationID = fpt.ToLocationID
|
||||
WHERE fpf.FloorPlanID = @FloorPlanID
|
||||
ORDER BY fpf.SortOrder, fromPath.LocationName, toPath.LocationName, fpt.TransitionType;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.FloorPlanFloor_Get
|
||||
@FloorPlanFloorID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT
|
||||
fpf.FloorPlanFloorID,
|
||||
fpf.FloorPlanID,
|
||||
fpf.LocationID,
|
||||
lp.LocationName,
|
||||
lp.LocationPath,
|
||||
fpf.Name,
|
||||
fpf.Description,
|
||||
fpf.SortOrder,
|
||||
fpf.GridWidth,
|
||||
fpf.GridHeight,
|
||||
fpf.BackgroundImagePath,
|
||||
fpf.BackgroundImageOriginalFileName,
|
||||
COALESCE(fpf.BackgroundOpacity, CONVERT(decimal(5,2), 0.35)) AS BackgroundOpacity,
|
||||
COALESCE(fpf.BackgroundScale, CONVERT(decimal(8,3), 1.000)) AS BackgroundScale,
|
||||
COALESCE(fpf.BackgroundOffsetX, CONVERT(decimal(10,2), 0.00)) AS BackgroundOffsetX,
|
||||
COALESCE(fpf.BackgroundOffsetY, CONVERT(decimal(10,2), 0.00)) AS BackgroundOffsetY,
|
||||
fpf.BackgroundLocked,
|
||||
fpf.CreatedDate,
|
||||
fpf.ModifiedDate
|
||||
FROM dbo.FloorPlanFloors fpf
|
||||
LEFT JOIN dbo.LocationPaths lp ON lp.LocationID = fpf.LocationID
|
||||
WHERE fpf.FloorPlanFloorID = @FloorPlanFloorID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.FloorPlanFloor_ListByPlan
|
||||
@FloorPlanID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
SELECT
|
||||
fpf.FloorPlanFloorID,
|
||||
fpf.FloorPlanID,
|
||||
fpf.LocationID,
|
||||
lp.LocationName,
|
||||
lp.LocationPath,
|
||||
fpf.Name,
|
||||
fpf.Description,
|
||||
fpf.SortOrder,
|
||||
fpf.GridWidth,
|
||||
fpf.GridHeight,
|
||||
fpf.BackgroundImagePath,
|
||||
fpf.BackgroundImageOriginalFileName,
|
||||
fpf.BackgroundOpacity,
|
||||
fpf.BackgroundScale,
|
||||
fpf.BackgroundOffsetX,
|
||||
fpf.BackgroundOffsetY,
|
||||
fpf.BackgroundLocked,
|
||||
fpf.CreatedDate,
|
||||
fpf.ModifiedDate
|
||||
FROM dbo.FloorPlanFloors fpf
|
||||
LEFT JOIN dbo.LocationPaths lp ON lp.LocationID = fpf.LocationID
|
||||
WHERE fpf.FloorPlanID = @FloorPlanID
|
||||
ORDER BY fpf.SortOrder, fpf.Name;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.FloorPlanFloor_Save
|
||||
@FloorPlanFloorID int,
|
||||
@FloorPlanID int,
|
||||
@LocationID int = NULL,
|
||||
@Name nvarchar(100),
|
||||
@Description nvarchar(max) = NULL,
|
||||
@SortOrder int,
|
||||
@GridWidth int,
|
||||
@GridHeight int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
DECLARE @ProjectID int;
|
||||
SELECT @ProjectID = ProjectID FROM dbo.FloorPlans WHERE FloorPlanID = @FloorPlanID;
|
||||
|
||||
IF @ProjectID IS NULL
|
||||
THROW 51003, 'Floor plan not found.', 1;
|
||||
|
||||
SET @Name = NULLIF(LTRIM(RTRIM(@Name)), N'');
|
||||
IF @Name IS NULL
|
||||
THROW 51001, 'Floor name is required.', 1;
|
||||
|
||||
IF @GridWidth NOT BETWEEN 8 AND 80 OR @GridHeight NOT BETWEEN 8 AND 80
|
||||
THROW 51002, 'Grid width and height must be between 8 and 80.', 1;
|
||||
|
||||
IF @LocationID IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM dbo.Locations WHERE LocationID = @LocationID AND ProjectID = @ProjectID AND IsArchived = 0)
|
||||
THROW 51008, 'Floor location must belong to this project.', 1;
|
||||
|
||||
IF @FloorPlanFloorID = 0
|
||||
BEGIN
|
||||
INSERT dbo.FloorPlanFloors (FloorPlanID, LocationID, Name, Description, SortOrder, GridWidth, GridHeight, BackgroundOpacity, BackgroundScale, BackgroundOffsetX, BackgroundOffsetY, BackgroundLocked)
|
||||
VALUES (@FloorPlanID, @LocationID, @Name, @Description, @SortOrder, @GridWidth, @GridHeight, 0.35, 1.000, 0.00, 0.00, 1);
|
||||
|
||||
SELECT CONVERT(int, SCOPE_IDENTITY());
|
||||
RETURN;
|
||||
END;
|
||||
|
||||
UPDATE dbo.FloorPlanFloors
|
||||
SET LocationID = @LocationID,
|
||||
Name = @Name,
|
||||
Description = @Description,
|
||||
SortOrder = @SortOrder,
|
||||
GridWidth = @GridWidth,
|
||||
GridHeight = @GridHeight,
|
||||
ModifiedDate = SYSUTCDATETIME()
|
||||
WHERE FloorPlanFloorID = @FloorPlanFloorID
|
||||
AND FloorPlanID = @FloorPlanID;
|
||||
|
||||
SELECT @FloorPlanFloorID;
|
||||
END;
|
||||
GO
|
||||
@ -1935,6 +1935,7 @@ public sealed class FloorPlanEditorViewModel
|
||||
public IReadOnlyList<SelectListItem> TransitionTypeOptions { get; set; } = [];
|
||||
public IReadOnlyList<FloorPlanOccupancySceneViewModel> OccupancyScenes { get; set; } = [];
|
||||
public IReadOnlyList<FloorPlanOccupancyTokenViewModel> OccupancyTokens { get; set; } = [];
|
||||
public IReadOnlyList<FloorPlanOccupancyWarningViewModel> OccupancyWarnings { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class FloorPlanOccupancySceneViewModel
|
||||
@ -1957,6 +1958,14 @@ public sealed class FloorPlanOccupancyTokenViewModel
|
||||
public string? ThumbnailPath { get; set; }
|
||||
}
|
||||
|
||||
public sealed class FloorPlanOccupancyWarningViewModel
|
||||
{
|
||||
public int SceneID { get; set; }
|
||||
public int? FloorPlanFloorID { get; set; }
|
||||
public string Severity { get; set; } = "warning";
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class FloorPlanFloorEditViewModel
|
||||
{
|
||||
public int FloorPlanFloorID { get; set; }
|
||||
@ -1968,6 +1977,8 @@ public sealed class FloorPlanFloorEditViewModel
|
||||
[Required, StringLength(100)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public string? Description { get; set; }
|
||||
|
||||
public int SortOrder { get; set; }
|
||||
|
||||
[Range(8, 80)]
|
||||
|
||||
@ -103,32 +103,6 @@
|
||||
<div class="alert alert-danger">@error</div>
|
||||
}
|
||||
|
||||
<section class="edit-panel floor-plan-meta floor-plan-meta-wide">
|
||||
<form asp-action="SavePlan" method="post" class="row g-3">
|
||||
<input asp-for="FloorPlan.FloorPlanID" type="hidden" name="FloorPlanID" />
|
||||
<input asp-for="FloorPlan.ProjectID" type="hidden" name="ProjectID" />
|
||||
<div class="col-md-4">
|
||||
<label asp-for="FloorPlan.Name" class="form-label"></label>
|
||||
<input asp-for="FloorPlan.Name" class="form-control" name="Name" />
|
||||
<span asp-validation-for="FloorPlan.Name" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label asp-for="FloorPlan.Description" class="form-label"></label>
|
||||
<input asp-for="FloorPlan.Description" class="form-control" name="Description" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Root Location</label>
|
||||
<select asp-for="FloorPlan.LocationID" asp-items="Model.LocationOptions" class="form-select" name="LocationID"></select>
|
||||
</div>
|
||||
<div class="col-md-2 d-flex align-items-end">
|
||||
<button class="btn btn-outline-primary w-100" type="submit">Save details</button>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<p class="muted mb-0">Linked Location: @(string.IsNullOrWhiteSpace(Model.FloorPlan.LocationPath) ? "A matching Location will be created or linked on save." : Model.FloorPlan.LocationPath)</p>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div data-floor-plan-workspace data-floor-plan-id="@Model.FloorPlan.FloorPlanID">
|
||||
@if (!Model.Floors.Any())
|
||||
{
|
||||
@ -226,6 +200,21 @@
|
||||
</label>
|
||||
<button class="btn btn-outline-secondary btn-sm" type="button" data-occupancy-prev>Previous Scene</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" type="button" data-occupancy-next>Next Scene</button>
|
||||
<div class="floor-plan-occupancy-warning-panel" data-occupancy-warning-panel>
|
||||
<strong>Occupancy Warnings</strong>
|
||||
<p class="floor-plan-occupancy-warning-empty" data-occupancy-warning-empty>All scene occupants are placed.</p>
|
||||
<ul>
|
||||
@foreach (var warning in Model.OccupancyWarnings)
|
||||
{
|
||||
<li class="floor-plan-occupancy-warning floor-plan-occupancy-warning--@warning.Severity"
|
||||
data-occupancy-warning
|
||||
data-occupancy-scene="@warning.SceneID"
|
||||
data-occupancy-floor="@warning.FloorPlanFloorID">
|
||||
@warning.Message
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -722,11 +711,18 @@
|
||||
<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].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">Description</label>
|
||||
<textarea asp-for="Floors[i].Description" class="form-control form-control-sm" rows="2"></textarea>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Root Location</label>
|
||||
<select asp-for="Floors[i].LocationID" asp-items="Model.LocationOptions" class="form-select form-select-sm" data-floor-location-select="@floor.FloorPlanFloorID"></select>
|
||||
</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-input="@floor.FloorPlanFloorID" />
|
||||
@ -1014,6 +1010,8 @@
|
||||
const occupancyNext = editor.querySelector("[data-occupancy-next]");
|
||||
const occupancyTrays = [...editor.querySelectorAll("[data-occupancy-tray]")];
|
||||
const occupancyMessages = [...editor.querySelectorAll("[data-occupancy-message]")];
|
||||
const occupancyWarnings = [...editor.querySelectorAll("[data-occupancy-warning]")];
|
||||
const occupancyWarningEmpty = editor.querySelector("[data-occupancy-warning-empty]");
|
||||
const initialSelectedBlockId = options.selectedBlockId || "@selectedBlockId";
|
||||
const saveStatusText = {
|
||||
saved: "Saved",
|
||||
@ -1098,6 +1096,18 @@
|
||||
occupancyMessages.forEach(message => {
|
||||
message.hidden = !showOccupancy || message.dataset.occupancyScene !== sceneId || message.dataset.occupancyFloor !== activeFloorId;
|
||||
});
|
||||
let visibleWarnings = 0;
|
||||
occupancyWarnings.forEach(warning => {
|
||||
const warningFloorId = warning.dataset.occupancyFloor || "";
|
||||
const visible = showOccupancy
|
||||
&& warning.dataset.occupancyScene === sceneId
|
||||
&& (!warningFloorId || warningFloorId === activeFloorId);
|
||||
warning.hidden = !visible;
|
||||
if (visible) visibleWarnings++;
|
||||
});
|
||||
if (occupancyWarningEmpty) {
|
||||
occupancyWarningEmpty.hidden = !showOccupancy || visibleWarnings > 0;
|
||||
}
|
||||
}
|
||||
|
||||
function setFloorPlanMode(mode) {
|
||||
@ -1262,6 +1272,8 @@
|
||||
activeFloorId: activePanel?.dataset.floorPanel || tabs.find(tab => tab.classList.contains("active"))?.dataset.floorTab || "",
|
||||
activeToolboxTab: toolboxTabs.find(tab => tab.classList.contains("active"))?.dataset.toolboxTab || "add",
|
||||
selectedBlockId: selectedBlock?.dataset.block || "",
|
||||
floorPlanMode: modeButtons.find(button => button.classList.contains("active"))?.dataset.floorPlanMode || "structure",
|
||||
occupancySceneId: selectedOccupancySceneId(),
|
||||
pageX: window.scrollX,
|
||||
pageY: window.scrollY,
|
||||
gridLeft: gridWrap?.scrollLeft || 0,
|
||||
@ -1300,6 +1312,12 @@
|
||||
if (state.activeToolboxTab) {
|
||||
setToolboxTab(state.activeToolboxTab);
|
||||
}
|
||||
if (state.occupancySceneId && occupancySceneSelect) {
|
||||
occupancySceneSelect.value = String(state.occupancySceneId);
|
||||
}
|
||||
if (state.floorPlanMode) {
|
||||
setFloorPlanMode(state.floorPlanMode);
|
||||
}
|
||||
if (state.selectedBlockId) {
|
||||
const block = blocks.find(item => item.dataset.block === String(state.selectedBlockId));
|
||||
if (block) {
|
||||
@ -1347,7 +1365,9 @@
|
||||
window.initializeFloorPlanEditor({
|
||||
activeFloorId: state.activeFloorId,
|
||||
activeToolboxTab: state.activeToolboxTab,
|
||||
selectedBlockId: state.selectedBlockId
|
||||
selectedBlockId: state.selectedBlockId,
|
||||
floorPlanMode: state.floorPlanMode,
|
||||
occupancySceneId: state.occupancySceneId
|
||||
});
|
||||
const nextEditor = document.querySelector("[data-floor-plan-editor]");
|
||||
const nextStatus = nextEditor?.querySelector("[data-save-status]");
|
||||
@ -2403,7 +2423,9 @@
|
||||
window.initializeFloorPlanEditor({
|
||||
activeFloorId: state.activeFloorId,
|
||||
activeToolboxTab: state.activeToolboxTab,
|
||||
selectedBlockId: state.selectedBlockId
|
||||
selectedBlockId: state.selectedBlockId,
|
||||
floorPlanMode: state.floorPlanMode,
|
||||
occupancySceneId: state.occupancySceneId
|
||||
});
|
||||
window.requestAnimationFrame(() => {
|
||||
const refreshedEditor = document.querySelector("[data-floor-plan-editor]");
|
||||
@ -2517,6 +2539,13 @@
|
||||
input.addEventListener("change", () => updateFloorLevel(true));
|
||||
});
|
||||
|
||||
editor.querySelectorAll("[data-floor-location-select]").forEach(input => {
|
||||
input.addEventListener("change", () => {
|
||||
refreshAfterSave = true;
|
||||
markDirty();
|
||||
});
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
@ -4575,6 +4575,62 @@ body.dragging-location [data-drag-type="location"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.floor-plan-occupancy-warning-panel {
|
||||
background: rgba(47, 111, 99, .05);
|
||||
border: 1px solid rgba(47, 111, 99, .16);
|
||||
border-radius: 8px;
|
||||
color: var(--plotline-ink, #1e252b);
|
||||
flex: 1 1 100%;
|
||||
font-size: .82rem;
|
||||
padding: .55rem .7rem;
|
||||
}
|
||||
|
||||
.floor-plan-occupancy-warning-panel strong {
|
||||
display: block;
|
||||
font-size: .78rem;
|
||||
margin-bottom: .25rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.floor-plan-occupancy-warning-panel p,
|
||||
.floor-plan-occupancy-warning-panel ul {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.floor-plan-occupancy-warning-panel ul {
|
||||
display: grid;
|
||||
gap: .25rem;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.floor-plan-occupancy-warning,
|
||||
.floor-plan-occupancy-warning-empty {
|
||||
border-radius: 6px;
|
||||
font-weight: 700;
|
||||
padding: .32rem .45rem;
|
||||
}
|
||||
|
||||
.floor-plan-occupancy-warning-empty {
|
||||
background: rgba(47, 111, 99, .08);
|
||||
color: var(--plotline-accent-dark, #22534a);
|
||||
}
|
||||
|
||||
.floor-plan-occupancy-warning--warning {
|
||||
background: rgba(251, 236, 211, .78);
|
||||
color: #7b4b12;
|
||||
}
|
||||
|
||||
.floor-plan-occupancy-warning--info {
|
||||
background: rgba(233, 243, 248, .86);
|
||||
color: #245b78;
|
||||
}
|
||||
|
||||
.floor-plan-occupancy-warning[hidden],
|
||||
.floor-plan-occupancy-warning-empty[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.floor-plan-editor-toolbar .muted {
|
||||
display: block;
|
||||
font-size: .76rem;
|
||||
@ -5582,6 +5638,31 @@ body.dragging-location [data-drag-type="location"] {
|
||||
color: #d9c8b8;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .floor-plan-occupancy-warning-panel,
|
||||
.dark .floor-plan-occupancy-warning-panel {
|
||||
background: rgba(244, 234, 220, .04);
|
||||
border-color: rgba(212, 154, 98, .2);
|
||||
color: #f1e7dd;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .floor-plan-occupancy-warning-empty,
|
||||
.dark .floor-plan-occupancy-warning-empty {
|
||||
background: rgba(47, 111, 99, .18);
|
||||
color: #b8e2d7;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .floor-plan-occupancy-warning--warning,
|
||||
.dark .floor-plan-occupancy-warning--warning {
|
||||
background: rgba(84, 57, 25, .92);
|
||||
color: #ffd9a5;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .floor-plan-occupancy-warning--info,
|
||||
.dark .floor-plan-occupancy-warning--info {
|
||||
background: rgba(24, 45, 58, .92);
|
||||
color: #b9dcf3;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .floor-plan-transition-accordion .accordion-item,
|
||||
.dark .floor-plan-transition-accordion .accordion-item {
|
||||
border-color: rgba(212, 154, 98, .22);
|
||||
|
||||
2
PlotLine/wwwroot/css/site.min.css
vendored
2
PlotLine/wwwroot/css/site.min.css
vendored
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user