Files
ephub/templates/board/plan/_spot.html.twig
T

86 lines
4.7 KiB
Twig

{#
The shared shape a plan is drawn from. Imported by each board/plan/_<type>.html.twig, so the
per-type partials carry geometry only and the states look identical everywhere.
The state comes from App\Booking\Spot, not from counting bookings here. That distinction is
the fix for a real bug: a desk booked 09:00-13:00 used to render exactly like one booked all
day on an all-day board, and carried no hx-get either -- so a second person could not book the
afternoon at all unless they happened to narrow the span by hand first.
Who holds what is listed INSIDE the shape rather than in an SVG <title> flyout: a tooltip only
appears on hover, so it is useless on touch and invisible when scanning the plan, which is the
one thing a floor plan is for. The box therefore has a fixed layout -- label on top, one line
per booking below -- and the per-type partials size their boxes to fit MAX_LINES of them.
Clicking always opens a modal: the create modal when anything is still free, the edit modal
when it is yours (which is where changing the times and releasing live). Nothing else is
clickable, so the SVG carries no confirm dialogs or inline CSRF tokens.
#}
{% macro spot(spot, geometry, width, height, selected_date, is_past, time_params) %}
{% set MAX_LINES = 3 %}
{% set state = spot.state.name %}
{% if state == 'MINE' %}
{% set fill = 'fill-primary' %}{% set stroke = 'stroke-primary-dark' %}{% set text = 'fill-white' %}
{% elseif state == 'TAKEN' %}
{% set fill = 'fill-gray-200' %}{% set stroke = 'stroke-gray-400' %}{% set text = 'fill-gray-600' %}
{% elseif state == 'PARTLY' %}
{% set fill = 'fill-amber-50' %}{% set stroke = 'stroke-amber-500' %}{% set text = 'fill-amber-900' %}
{% else %}
{% set fill = 'fill-emerald-50' %}{% set stroke = 'stroke-emerald-500' %}{% set text = 'fill-emerald-900' %}
{% endif %}
{% set interactive = not is_past and spot.actionable %}
{# One line per booking, then the window still free if there is one -- that last line is the
reason a partly booked shape is worth clicking, so it earns its place. #}
{% set lines = [] %}
{% for booking in spot.bookings %}
{% if booking.user.id == app.user.id %}
{% set who = 'du' %}
{% else %}
{# "Anna Berger" -> "Anna B." : full names do not fit a 140px box at this size. #}
{% set parts = booking.user.name|split(' ')|filter(part => part is not empty) %}
{% set who = parts|length > 1 ? parts|first ~ ' ' ~ parts|last|slice(0, 1) ~ '.' : booking.user.name %}
{% endif %}
{% set lines = lines|merge([who ~ (booking.timeRange.allDay ? '' : ' ' ~ booking.timeRange)]) %}
{% endfor %}
{% if state == 'PARTLY' %}
{% set lines = lines|merge(['frei ' ~ spot.suggested]) %}
{% endif %}
{# Overflow rather than spill out of the box. Two lines plus a counter beats three lines and
a silent omission. #}
{% if lines|length > MAX_LINES %}
{% set lines = lines|slice(0, MAX_LINES - 1)|merge(['+' ~ (lines|length - MAX_LINES + 1) ~ ' weitere']) %}
{% endif %}
<g class="{{ interactive ? 'cursor-pointer' : 'cursor-default' }}"
{%- if interactive %} role="button"{% endif %}
aria-label="{{ spot.resource.label }}{% if lines is empty %} — frei{% else %} — {{ lines|join(', ') }}{% endif %}"
{%- if interactive and state == 'MINE' %}
hx-get="{{ path('app_booking_edit', { id: spot.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.
It prefills the free window itself -- see BookingController::create. #}
hx-get="{{ path('app_booking_new', { id: spot.resource.id, date: selected_date|date('Y-m-d') }|merge(time_params)) }}"
hx-target="body"
hx-swap="beforeend"
{%- endif %}>
<rect x="{{ geometry.x }}" y="{{ geometry.y }}" width="{{ width }}" height="{{ height }}" rx="6"
class="{{ fill }} {{ stroke }}" stroke-width="2"></rect>
{# Nothing to list: the colour already says "free", so the label sits centred as before. #}
<text x="{{ geometry.x + width / 2 }}" y="{{ geometry.y + (lines is empty ? height / 2 + 4 : 18) }}"
text-anchor="middle" class="{{ text }} text-xs font-semibold">{{ spot.resource.label }}</text>
{% for line in lines %}
<text x="{{ geometry.x + width / 2 }}" y="{{ geometry.y + 34 + loop.index0 * 13 }}"
text-anchor="middle" class="{{ text }} text-[10px]">{{ line }}</text>
{% endfor %}
</g>
{% endmacro %}