120 lines
6.5 KiB
Twig
120 lines
6.5 KiB
Twig
{#
|
|
Hand-authored floor plan. The geometry below is the drawing; `svgElementId` on each Resource
|
|
is the key that ties a database row to a shape here. A resource whose id is missing from the
|
|
map is simply not drawn (and vice versa), which is why app:seed and this map have to agree.
|
|
|
|
Colouring needs no JavaScript at all: the SVG is re-rendered server side for the selected day
|
|
and span, so free/belegt/deine is decided in Twig. Note "free" is always relative to the
|
|
span in the header -- a desk booked 09:00-13:00 is free for an afternoon board and belegt for
|
|
an all-day one.
|
|
|
|
Clicking a shape always opens a modal: the create modal when it is free, the edit modal when
|
|
it is yours (which is where changing the times and releasing it live). Nothing else is
|
|
clickable, so the SVG carries no confirm dialogs or inline tokens.
|
|
#}
|
|
{% set desk_plan = {
|
|
'desk-01': { x: 50, y: 70 }, 'desk-02': { x: 190, y: 70 }, 'desk-03': { x: 330, y: 70 },
|
|
'desk-04': { x: 50, y: 170 }, 'desk-05': { x: 190, y: 170 }, 'desk-06': { x: 330, y: 170 },
|
|
'desk-07': { x: 50, y: 270 }, 'desk-08': { x: 190, y: 270 }, 'desk-09': { x: 330, y: 270 },
|
|
'desk-10': { x: 50, y: 370 }, 'desk-11': { x: 190, y: 370 }, 'desk-12': { x: 330, y: 370 }
|
|
} %}
|
|
{% set parking_plan = {
|
|
'parking-01': { x: 540, y: 60 }, 'parking-02': { x: 540, y: 125 }, 'parking-03': { x: 540, y: 190 },
|
|
'parking-04': { x: 540, y: 255 }, 'parking-05': { x: 540, y: 320 }, 'parking-06': { x: 540, y: 385 }
|
|
} %}
|
|
{% set time_params = selected_time.allDay ? {} : { von: selected_time.start, bis: selected_time.end } %}
|
|
|
|
{% macro spot(resource, geometry, width, height, bookings, selected_date, is_past, time_params) %}
|
|
{% set own = bookings|filter(b => b.user.id == app.user.id)|first|default(null) %}
|
|
{% set mine = own is not null %}
|
|
|
|
{% if mine %}
|
|
{% set fill = 'fill-primary' %}{% set stroke = 'stroke-primary-dark' %}{% set text = 'fill-white' %}
|
|
{% elseif bookings is not empty %}
|
|
{% set fill = 'fill-gray-200' %}{% set stroke = 'stroke-gray-400' %}{% set text = 'fill-gray-600' %}
|
|
{% else %}
|
|
{% set fill = 'fill-emerald-50' %}{% set stroke = 'stroke-emerald-500' %}{% set text = 'fill-emerald-900' %}
|
|
{% endif %}
|
|
|
|
{% set interactive = not is_past and (mine or bookings is empty) %}
|
|
|
|
{# What the second line of the shape says. Several bookings can overlap the board's span once
|
|
part-day bookings exist, so the shape counts them and the tooltip spells them out. #}
|
|
{% if mine %}
|
|
{% set caption = own.timeRange.allDay ? 'du' : 'du ' ~ own.timeRange %}
|
|
{% elseif bookings|length > 1 %}
|
|
{% set caption = bookings|length ~ ' Buchungen' %}
|
|
{% elseif bookings is not empty %}
|
|
{% set caption = bookings|first.user.name %}
|
|
{% else %}
|
|
{% set caption = null %}
|
|
{% endif %}
|
|
|
|
<g class="{{ interactive ? 'cursor-pointer' : 'cursor-default' }}"
|
|
{%- if interactive and mine %}
|
|
hx-get="{{ path('app_booking_edit', { id: own.id }|merge(time_params)) }}"
|
|
hx-target="body"
|
|
hx-swap="beforeend"
|
|
{%- elseif interactive %}
|
|
{# The modal is appended to the end of <body>; closing it is Stimulus, not htmx. #}
|
|
hx-get="{{ path('app_booking_new', { id: resource.id, date: selected_date|date('Y-m-d') }|merge(time_params)) }}"
|
|
hx-target="body"
|
|
hx-swap="beforeend"
|
|
{%- endif %}>
|
|
<title>
|
|
{{- resource.label }}
|
|
{%- if bookings is empty %} — frei
|
|
{%- else %}
|
|
{%- for booking in bookings %} — {{ booking.user.id == app.user.id ? 'du' : booking.user.name }} {{ booking.timeRange }}{% endfor %}
|
|
{%- endif -%}
|
|
</title>
|
|
<rect x="{{ geometry.x }}" y="{{ geometry.y }}" width="{{ width }}" height="{{ height }}" rx="6"
|
|
class="{{ fill }} {{ stroke }}" stroke-width="2"></rect>
|
|
<text x="{{ geometry.x + width / 2 }}" y="{{ geometry.y + (caption ? height / 2 - 4 : height / 2 + 4) }}"
|
|
text-anchor="middle" class="{{ text }} text-xs font-semibold">{{ resource.label }}</text>
|
|
{% if caption %}
|
|
<text x="{{ geometry.x + width / 2 }}" y="{{ geometry.y + height / 2 + 12 }}"
|
|
text-anchor="middle" class="{{ text }} text-[10px]">{{ caption }}</text>
|
|
{% endif %}
|
|
</g>
|
|
{% endmacro %}
|
|
|
|
<div class="rounded-md border border-gray-200 p-4">
|
|
<div class="flex flex-wrap items-baseline justify-between gap-2 pb-2">
|
|
<h2 class="!pb-0">{{ selected_date|format_datetime(pattern: 'EEEE, d. MMMM y') }}</h2>
|
|
<div class="flex items-center gap-4 text-xs text-gray-500">
|
|
<span class="flex items-center gap-1"><span class="w-3 h-3 rounded-sm bg-emerald-50 border-2 border-emerald-500"></span> frei</span>
|
|
<span class="flex items-center gap-1"><span class="w-3 h-3 rounded-sm bg-gray-200 border-2 border-gray-400"></span> belegt</span>
|
|
<span class="flex items-center gap-1"><span class="w-3 h-3 rounded-sm bg-primary border-2 border-primary-dark"></span> deine</span>
|
|
</div>
|
|
</div>
|
|
|
|
{% include 'dashboard/_timerange.html.twig' %}
|
|
|
|
{% if is_past %}
|
|
<p class="pb-4 text-sm text-amber-700">Vergangener Tag — hier lässt sich nichts mehr buchen.</p>
|
|
{% endif %}
|
|
|
|
<svg viewBox="0 0 810 480" class="w-full h-auto" role="img" aria-label="Raumplan">
|
|
<rect x="20" y="30" width="470" height="420" rx="8" class="fill-white stroke-gray-300" stroke-width="2"></rect>
|
|
<text x="30" y="22" class="fill-gray-400 text-xs font-semibold">Büro</text>
|
|
|
|
<rect x="520" y="30" width="270" height="420" rx="8" class="fill-white stroke-gray-300" stroke-width="2"></rect>
|
|
<text x="530" y="22" class="fill-gray-400 text-xs font-semibold">Parkplatz</text>
|
|
|
|
{% for resource in desks %}
|
|
{% if resource.svgElementId and desk_plan[resource.svgElementId] is defined %}
|
|
{{ _self.spot(resource, desk_plan[resource.svgElementId], 120, 70,
|
|
bookings_by_resource[resource.id]|default([]), selected_date, is_past, time_params) }}
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
{% for resource in parking %}
|
|
{% if resource.svgElementId and parking_plan[resource.svgElementId] is defined %}
|
|
{{ _self.spot(resource, parking_plan[resource.svgElementId], 230, 50,
|
|
bookings_by_resource[resource.id]|default([]), selected_date, is_past, time_params) }}
|
|
{% endif %}
|
|
{% endfor %}
|
|
</svg>
|
|
</div>
|