feat: render the floor plan in html instead of svg
This commit is contained in:
1 parent
5a01054b0e
commit
ad5d5acb00
20 files changed
+256
-356
No files matched your search
+12
-20
@@ -24,12 +24,10 @@ use Symfony\Component\DependencyInjection\Attribute\When;
|
||||
* Seeds the office rooms, the desks, the parking spots and a handful of employees.
|
||||
*
|
||||
* Dev only, and idempotent: it matches on the natural key (group and resource label, user email)
|
||||
* so it can be re-run after a migration without duplicating anything. The svgElementId values --
|
||||
* on both resources and groups -- are the contract with
|
||||
* templates/board/plan/_<type>.html.twig: change one and the other stops drawing it.
|
||||
* so it can be re-run after a migration without duplicating anything.
|
||||
*
|
||||
* The room names and their desk membership are placeholders standing in for the real office
|
||||
* layout, and they only make sense together with the geometry in _desk.html.twig.
|
||||
* layout.
|
||||
*/
|
||||
#[When('dev')]
|
||||
#[AsCommand(name: 'app:seed', description: 'Seeds desks, parking spots and demo employees (dev only)')]
|
||||
@@ -50,15 +48,10 @@ final class SeedCommand extends Command
|
||||
|
||||
$created = 0;
|
||||
|
||||
// Four rooms of three desks, in the order they are drawn down the plan.
|
||||
// Four rooms of three desks, in the order they are listed on the plan.
|
||||
$rooms = [];
|
||||
foreach ([
|
||||
['Raum Nord', 'room-01'],
|
||||
['Raum Ost', 'room-02'],
|
||||
['Raum Süd', 'room-03'],
|
||||
['Besprechungsraum', 'room-04'],
|
||||
] as $position => [$label, $svgElementId]) {
|
||||
[$room, $isNew] = $this->ensureGroup(ResourceType::DESK, $label, $position, $svgElementId);
|
||||
foreach (['Raum Nord', 'Raum Ost', 'Raum Süd', 'Besprechungsraum'] as $position => $label) {
|
||||
[$room, $isNew] = $this->ensureGroup(ResourceType::DESK, $label, $position);
|
||||
$rooms[] = $room;
|
||||
$created += $isNew;
|
||||
}
|
||||
@@ -67,15 +60,14 @@ final class SeedCommand extends Command
|
||||
$created += $this->ensureResource(
|
||||
ResourceType::DESK,
|
||||
\sprintf('Schreibtisch %02d', $i),
|
||||
\sprintf('desk-%02d', $i),
|
||||
$rooms[intdiv($i - 1, 3)],
|
||||
);
|
||||
}
|
||||
|
||||
// Parking is deliberately left ungrouped: the car park is one area, and it is what keeps
|
||||
// the "a plan without groups draws its own frame" path exercised by real data.
|
||||
// the "a plan without rooms" path exercised by real data.
|
||||
for ($i = 1; $i <= 6; ++$i) {
|
||||
$created += $this->ensureResource(ResourceType::PARKING, \sprintf('Parkplatz %02d', $i), \sprintf('parking-%02d', $i));
|
||||
$created += $this->ensureResource(ResourceType::PARKING, \sprintf('Parkplatz %02d', $i));
|
||||
}
|
||||
|
||||
// One administrator on purpose, so the dev environment exercises both roles that open
|
||||
@@ -97,13 +89,13 @@ final class SeedCommand extends Command
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function ensureResource(ResourceType $type, string $label, string $svgElementId, ?ResourceGroup $group = null): int
|
||||
private function ensureResource(ResourceType $type, string $label, ?ResourceGroup $group = null): int
|
||||
{
|
||||
$resource = $this->resources->findOneBy(['type' => $type, 'label' => $label]);
|
||||
|
||||
if (null !== $resource) {
|
||||
// Backfill only. A resource seeded before groups existed has no room and would never
|
||||
// be drawn inside one, but a group somebody moved it into by hand is left alone.
|
||||
// be listed under one, but a group somebody moved it into by hand is left alone.
|
||||
if (null !== $group && null === $resource->getGroup()) {
|
||||
$resource->setGroup($group);
|
||||
}
|
||||
@@ -111,7 +103,7 @@ final class SeedCommand extends Command
|
||||
return 0;
|
||||
}
|
||||
|
||||
$resource = new Resource($type, $label, $svgElementId);
|
||||
$resource = new Resource($type, $label);
|
||||
$resource->setGroup($group);
|
||||
$this->em->persist($resource);
|
||||
|
||||
@@ -121,7 +113,7 @@ final class SeedCommand extends Command
|
||||
/**
|
||||
* @return array{ResourceGroup, int} the group, and 1 if it was created
|
||||
*/
|
||||
private function ensureGroup(ResourceType $type, string $label, int $position, string $svgElementId): array
|
||||
private function ensureGroup(ResourceType $type, string $label, int $position): array
|
||||
{
|
||||
$group = $this->groups->findOneBy(['type' => $type, 'label' => $label]);
|
||||
|
||||
@@ -129,7 +121,7 @@ final class SeedCommand extends Command
|
||||
return [$group, 0];
|
||||
}
|
||||
|
||||
$group = new ResourceGroup($type, $label, $position, $svgElementId);
|
||||
$group = new ResourceGroup($type, $label, $position);
|
||||
$this->em->persist($group);
|
||||
|
||||
return [$group, 1];
|
||||
|
||||
+2
-20
@@ -28,16 +28,9 @@ class Resource
|
||||
#[Assert\NotBlank]
|
||||
private string $label;
|
||||
|
||||
/**
|
||||
* Id of the matching element in the hand-authored floor plan SVG. Null for anything that is
|
||||
* bookable but not drawn on the plan.
|
||||
*/
|
||||
#[ORM\Column(length: 50, nullable: true)]
|
||||
private ?string $svgElementId = null;
|
||||
|
||||
/**
|
||||
* Where it physically is. Nullable because a group is optional: a kind of resource that is
|
||||
* not split across rooms simply has none, and its plan draws its own single frame.
|
||||
* not split across rooms simply has none, and the plan lists it without a room heading.
|
||||
*/
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
|
||||
@@ -50,11 +43,10 @@ class Resource
|
||||
#[ORM\Column]
|
||||
private bool $active = true;
|
||||
|
||||
public function __construct(ResourceType $type, string $label, ?string $svgElementId = null)
|
||||
public function __construct(ResourceType $type, string $label)
|
||||
{
|
||||
$this->type = $type;
|
||||
$this->label = $label;
|
||||
$this->svgElementId = $svgElementId;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -77,16 +69,6 @@ class Resource
|
||||
$this->label = $label;
|
||||
}
|
||||
|
||||
public function getSvgElementId(): ?string
|
||||
{
|
||||
return $this->svgElementId;
|
||||
}
|
||||
|
||||
public function setSvgElementId(?string $svgElementId): void
|
||||
{
|
||||
$this->svgElementId = $svgElementId;
|
||||
}
|
||||
|
||||
public function getGroup(): ?ResourceGroup
|
||||
{
|
||||
return $this->group;
|
||||
|
||||
@@ -40,19 +40,11 @@ class ResourceGroup
|
||||
#[ORM\Column]
|
||||
private int $position = 0;
|
||||
|
||||
/**
|
||||
* Id of the matching frame in the hand-authored floor plan SVG -- the same role svgElementId
|
||||
* plays on Resource. Null for a group that is not drawn.
|
||||
*/
|
||||
#[ORM\Column(length: 50, nullable: true)]
|
||||
private ?string $svgElementId = null;
|
||||
|
||||
public function __construct(ResourceType $type, string $label, int $position = 0, ?string $svgElementId = null)
|
||||
public function __construct(ResourceType $type, string $label, int $position = 0)
|
||||
{
|
||||
$this->type = $type;
|
||||
$this->label = $label;
|
||||
$this->position = $position;
|
||||
$this->svgElementId = $svgElementId;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -84,14 +76,4 @@ class ResourceGroup
|
||||
{
|
||||
$this->position = $position;
|
||||
}
|
||||
|
||||
public function getSvgElementId(): ?string
|
||||
{
|
||||
return $this->svgElementId;
|
||||
}
|
||||
|
||||
public function setSvgElementId(?string $svgElementId): void
|
||||
{
|
||||
$this->svgElementId = $svgElementId;
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,9 @@ namespace App\Enum;
|
||||
* The kinds of thing that can be booked.
|
||||
*
|
||||
* **This enum is the extension point.** The dashboard builds one tile per case, the board route
|
||||
* accepts one backing value per case, and the floor plan is looked up by convention from that
|
||||
* same value. Adding a third bookable kind is therefore: a case here, a
|
||||
* `templates/board/plan/_<value>.html.twig` with its geometry, and rows in `app:seed`. Nothing
|
||||
* else needs editing — in particular no template lists the kinds by hand.
|
||||
* accepts one backing value per case, and the floor plan renders every kind the same way.
|
||||
* Adding a third bookable kind is therefore: a case here and rows in `app:seed`.
|
||||
* Nothing else needs editing — in particular no template lists the kinds by hand.
|
||||
*
|
||||
* The backing value is both what is persisted in `resource.type` and what appears in a URL, so
|
||||
* there is no slug mapping to keep in sync. The label, plural and description are the only
|
||||
|
||||
@@ -8,10 +8,10 @@ use App\Entity\ResourceGroup;
|
||||
use App\Enum\SpotState;
|
||||
|
||||
/**
|
||||
* The spots of one room, in draw order.
|
||||
* The spots of one room, in display order.
|
||||
*
|
||||
* The floor plan needs the resources of a room together so it can frame them, and it needs the
|
||||
* room's own label and geometry key alongside them -- which is all this is. The grouping is done
|
||||
* The floor plan needs the resources of a room together so it can list them under a heading, and
|
||||
* it needs the room's own label alongside them -- which is all this is. The grouping is done
|
||||
* once in the presenter rather than in Twig, because deciding what goes where is not a
|
||||
* presentation concern and a template cannot express it without a nested loop over every spot
|
||||
* per room.
|
||||
@@ -32,7 +32,7 @@ final readonly class SpotGroup
|
||||
*
|
||||
* Ungrouped spots collect into a single trailing entry whose group is null. That entry is
|
||||
* not an error case: a kind of resource that is not split across rooms has no groups at all,
|
||||
* and its plan draws its own frame instead.
|
||||
* and the plan lists its spots without a heading instead.
|
||||
*
|
||||
* @param list<Spot> $spots
|
||||
*
|
||||
@@ -56,7 +56,7 @@ final readonly class SpotGroup
|
||||
// instance per row, so this groups exactly as an id would -- and it also works for a
|
||||
// group that has not been persisted yet, where every id is still null and every
|
||||
// group would otherwise collapse into one. The repository orders by group, so
|
||||
// insertion order is already draw order.
|
||||
// insertion order is already display order.
|
||||
$grouped[spl_object_id($group)][] = $spot;
|
||||
}
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ final readonly class BoardContextFactory
|
||||
'is_past' => $selected < $today,
|
||||
'resources' => $resources,
|
||||
'spots' => $spots,
|
||||
// The same spots again, partitioned into the rooms the plan frames them with.
|
||||
// The same spots again, partitioned into the rooms the plan lists them under.
|
||||
'spot_groups' => SpotGroup::over(array_values($spots)),
|
||||
'my_bookings_today' => $mine[$selected->format('Y-m-d')] ?? [],
|
||||
];
|
||||
|
||||
Reference in new issue
Block a user