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();
|
using var connection = connectionFactory.CreateConnection();
|
||||||
return await connection.QuerySingleAsync<int>(
|
return await connection.QuerySingleAsync<int>(
|
||||||
"dbo.FloorPlanFloor_Save",
|
"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);
|
commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1167,6 +1167,7 @@ public sealed class FloorPlanFloor
|
|||||||
public string? LocationName { get; set; }
|
public string? LocationName { get; set; }
|
||||||
public string? LocationPath { get; set; }
|
public string? LocationPath { get; set; }
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string? Description { get; set; }
|
||||||
public int SortOrder { get; set; }
|
public int SortOrder { get; set; }
|
||||||
public int GridWidth { get; set; } = 24;
|
public int GridWidth { get; set; } = 24;
|
||||||
public int GridHeight { get; set; } = 16;
|
public int GridHeight { get; set; } = 16;
|
||||||
|
|||||||
@ -7733,6 +7733,8 @@ public sealed class FloorPlanService(
|
|||||||
IProjectRepository projects,
|
IProjectRepository projects,
|
||||||
ISceneRepository scenes,
|
ISceneRepository scenes,
|
||||||
ISceneFloorPlanOccupancyRepository sceneOccupancy,
|
ISceneFloorPlanOccupancyRepository sceneOccupancy,
|
||||||
|
ICharacterRepository characters,
|
||||||
|
IAssetRepository assets,
|
||||||
IFloorPlanRepository floorPlans,
|
IFloorPlanRepository floorPlans,
|
||||||
ILocationRepository locations,
|
ILocationRepository locations,
|
||||||
IFloorPlanBackgroundImageService backgroundImages,
|
IFloorPlanBackgroundImageService backgroundImages,
|
||||||
@ -7860,10 +7862,132 @@ public sealed class FloorPlanService(
|
|||||||
})
|
})
|
||||||
.Where(x => x is not null)
|
.Where(x => x is not null)
|
||||||
.Cast<FloorPlanOccupancyTokenViewModel>()
|
.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)
|
public async Task<int> SavePlanAsync(FloorPlanEditViewModel model)
|
||||||
{
|
{
|
||||||
var isNew = model.FloorPlanID == 0;
|
var isNew = model.FloorPlanID == 0;
|
||||||
@ -7915,6 +8039,7 @@ public sealed class FloorPlanService(
|
|||||||
FloorPlanID = model.FloorPlanID,
|
FloorPlanID = model.FloorPlanID,
|
||||||
LocationID = locationId,
|
LocationID = locationId,
|
||||||
Name = model.Name.Trim(),
|
Name = model.Name.Trim(),
|
||||||
|
Description = model.Description,
|
||||||
SortOrder = model.SortOrder,
|
SortOrder = model.SortOrder,
|
||||||
GridWidth = model.GridWidth,
|
GridWidth = model.GridWidth,
|
||||||
GridHeight = model.GridHeight
|
GridHeight = model.GridHeight
|
||||||
@ -8497,6 +8622,7 @@ public sealed class FloorPlanService(
|
|||||||
LocationName = floor.LocationName,
|
LocationName = floor.LocationName,
|
||||||
LocationPath = floor.LocationPath,
|
LocationPath = floor.LocationPath,
|
||||||
Name = floor.Name,
|
Name = floor.Name,
|
||||||
|
Description = floor.Description,
|
||||||
SortOrder = floor.SortOrder,
|
SortOrder = floor.SortOrder,
|
||||||
GridWidth = floor.GridWidth,
|
GridWidth = floor.GridWidth,
|
||||||
GridHeight = floor.GridHeight,
|
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<SelectListItem> TransitionTypeOptions { get; set; } = [];
|
||||||
public IReadOnlyList<FloorPlanOccupancySceneViewModel> OccupancyScenes { get; set; } = [];
|
public IReadOnlyList<FloorPlanOccupancySceneViewModel> OccupancyScenes { get; set; } = [];
|
||||||
public IReadOnlyList<FloorPlanOccupancyTokenViewModel> OccupancyTokens { get; set; } = [];
|
public IReadOnlyList<FloorPlanOccupancyTokenViewModel> OccupancyTokens { get; set; } = [];
|
||||||
|
public IReadOnlyList<FloorPlanOccupancyWarningViewModel> OccupancyWarnings { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class FloorPlanOccupancySceneViewModel
|
public sealed class FloorPlanOccupancySceneViewModel
|
||||||
@ -1957,6 +1958,14 @@ public sealed class FloorPlanOccupancyTokenViewModel
|
|||||||
public string? ThumbnailPath { get; set; }
|
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 sealed class FloorPlanFloorEditViewModel
|
||||||
{
|
{
|
||||||
public int FloorPlanFloorID { get; set; }
|
public int FloorPlanFloorID { get; set; }
|
||||||
@ -1968,6 +1977,8 @@ public sealed class FloorPlanFloorEditViewModel
|
|||||||
[Required, StringLength(100)]
|
[Required, StringLength(100)]
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
public int SortOrder { get; set; }
|
public int SortOrder { get; set; }
|
||||||
|
|
||||||
[Range(8, 80)]
|
[Range(8, 80)]
|
||||||
|
|||||||
@ -103,32 +103,6 @@
|
|||||||
<div class="alert alert-danger">@error</div>
|
<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">
|
<div data-floor-plan-workspace data-floor-plan-id="@Model.FloorPlan.FloorPlanID">
|
||||||
@if (!Model.Floors.Any())
|
@if (!Model.Floors.Any())
|
||||||
{
|
{
|
||||||
@ -226,6 +200,21 @@
|
|||||||
</label>
|
</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-prev>Previous Scene</button>
|
||||||
<button class="btn btn-outline-secondary btn-sm" type="button" data-occupancy-next>Next 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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -722,11 +711,18 @@
|
|||||||
<div class="floor-plan-floor-editor @(isActive ? string.Empty : "d-none")" data-floor-editor="@floor.FloorPlanFloorID">
|
<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].FloorPlanFloorID" type="hidden" />
|
||||||
<input asp-for="Floors[i].FloorPlanID" type="hidden" />
|
<input asp-for="Floors[i].FloorPlanID" type="hidden" />
|
||||||
<input asp-for="Floors[i].LocationID" type="hidden" />
|
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label">Floor name</label>
|
<label class="form-label">Floor name</label>
|
||||||
<input asp-for="Floors[i].Name" class="form-control form-control-sm" />
|
<input asp-for="Floors[i].Name" class="form-control form-control-sm" />
|
||||||
</div>
|
</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">
|
<div class="mb-2">
|
||||||
<label class="form-label">Level</label>
|
<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" />
|
<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 occupancyNext = editor.querySelector("[data-occupancy-next]");
|
||||||
const occupancyTrays = [...editor.querySelectorAll("[data-occupancy-tray]")];
|
const occupancyTrays = [...editor.querySelectorAll("[data-occupancy-tray]")];
|
||||||
const occupancyMessages = [...editor.querySelectorAll("[data-occupancy-message]")];
|
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 initialSelectedBlockId = options.selectedBlockId || "@selectedBlockId";
|
||||||
const saveStatusText = {
|
const saveStatusText = {
|
||||||
saved: "Saved",
|
saved: "Saved",
|
||||||
@ -1098,6 +1096,18 @@
|
|||||||
occupancyMessages.forEach(message => {
|
occupancyMessages.forEach(message => {
|
||||||
message.hidden = !showOccupancy || message.dataset.occupancyScene !== sceneId || message.dataset.occupancyFloor !== activeFloorId;
|
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) {
|
function setFloorPlanMode(mode) {
|
||||||
@ -1262,6 +1272,8 @@
|
|||||||
activeFloorId: activePanel?.dataset.floorPanel || tabs.find(tab => tab.classList.contains("active"))?.dataset.floorTab || "",
|
activeFloorId: activePanel?.dataset.floorPanel || tabs.find(tab => tab.classList.contains("active"))?.dataset.floorTab || "",
|
||||||
activeToolboxTab: toolboxTabs.find(tab => tab.classList.contains("active"))?.dataset.toolboxTab || "add",
|
activeToolboxTab: toolboxTabs.find(tab => tab.classList.contains("active"))?.dataset.toolboxTab || "add",
|
||||||
selectedBlockId: selectedBlock?.dataset.block || "",
|
selectedBlockId: selectedBlock?.dataset.block || "",
|
||||||
|
floorPlanMode: modeButtons.find(button => button.classList.contains("active"))?.dataset.floorPlanMode || "structure",
|
||||||
|
occupancySceneId: selectedOccupancySceneId(),
|
||||||
pageX: window.scrollX,
|
pageX: window.scrollX,
|
||||||
pageY: window.scrollY,
|
pageY: window.scrollY,
|
||||||
gridLeft: gridWrap?.scrollLeft || 0,
|
gridLeft: gridWrap?.scrollLeft || 0,
|
||||||
@ -1300,6 +1312,12 @@
|
|||||||
if (state.activeToolboxTab) {
|
if (state.activeToolboxTab) {
|
||||||
setToolboxTab(state.activeToolboxTab);
|
setToolboxTab(state.activeToolboxTab);
|
||||||
}
|
}
|
||||||
|
if (state.occupancySceneId && occupancySceneSelect) {
|
||||||
|
occupancySceneSelect.value = String(state.occupancySceneId);
|
||||||
|
}
|
||||||
|
if (state.floorPlanMode) {
|
||||||
|
setFloorPlanMode(state.floorPlanMode);
|
||||||
|
}
|
||||||
if (state.selectedBlockId) {
|
if (state.selectedBlockId) {
|
||||||
const block = blocks.find(item => item.dataset.block === String(state.selectedBlockId));
|
const block = blocks.find(item => item.dataset.block === String(state.selectedBlockId));
|
||||||
if (block) {
|
if (block) {
|
||||||
@ -1347,7 +1365,9 @@
|
|||||||
window.initializeFloorPlanEditor({
|
window.initializeFloorPlanEditor({
|
||||||
activeFloorId: state.activeFloorId,
|
activeFloorId: state.activeFloorId,
|
||||||
activeToolboxTab: state.activeToolboxTab,
|
activeToolboxTab: state.activeToolboxTab,
|
||||||
selectedBlockId: state.selectedBlockId
|
selectedBlockId: state.selectedBlockId,
|
||||||
|
floorPlanMode: state.floorPlanMode,
|
||||||
|
occupancySceneId: state.occupancySceneId
|
||||||
});
|
});
|
||||||
const nextEditor = document.querySelector("[data-floor-plan-editor]");
|
const nextEditor = document.querySelector("[data-floor-plan-editor]");
|
||||||
const nextStatus = nextEditor?.querySelector("[data-save-status]");
|
const nextStatus = nextEditor?.querySelector("[data-save-status]");
|
||||||
@ -2403,7 +2423,9 @@
|
|||||||
window.initializeFloorPlanEditor({
|
window.initializeFloorPlanEditor({
|
||||||
activeFloorId: state.activeFloorId,
|
activeFloorId: state.activeFloorId,
|
||||||
activeToolboxTab: state.activeToolboxTab,
|
activeToolboxTab: state.activeToolboxTab,
|
||||||
selectedBlockId: state.selectedBlockId
|
selectedBlockId: state.selectedBlockId,
|
||||||
|
floorPlanMode: state.floorPlanMode,
|
||||||
|
occupancySceneId: state.occupancySceneId
|
||||||
});
|
});
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
const refreshedEditor = document.querySelector("[data-floor-plan-editor]");
|
const refreshedEditor = document.querySelector("[data-floor-plan-editor]");
|
||||||
@ -2517,6 +2539,13 @@
|
|||||||
input.addEventListener("change", () => updateFloorLevel(true));
|
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 => {
|
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-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("[form^='floor-plan-background-'], [form^='floor-plan-transition-add-']")) return;
|
||||||
|
|||||||
@ -4575,6 +4575,62 @@ body.dragging-location [data-drag-type="location"] {
|
|||||||
width: 100%;
|
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 {
|
.floor-plan-editor-toolbar .muted {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: .76rem;
|
font-size: .76rem;
|
||||||
@ -5582,6 +5638,31 @@ body.dragging-location [data-drag-type="location"] {
|
|||||||
color: #d9c8b8;
|
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,
|
[data-bs-theme="dark"] .floor-plan-transition-accordion .accordion-item,
|
||||||
.dark .floor-plan-transition-accordion .accordion-item {
|
.dark .floor-plan-transition-accordion .accordion-item {
|
||||||
border-color: rgba(212, 154, 98, .22);
|
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