102 lines
5.4 KiB
Twig
102 lines
5.4 KiB
Twig
{#
|
|
One bookable thing on the plan: a card coloured by its state, with who holds it listed inside.
|
|
|
|
The state comes from App\Model\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 card rather than in a title tooltip: 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.
|
|
|
|
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 a card carries no confirm dialogs or inline CSRF tokens.
|
|
#}
|
|
{% macro spot(spot, selected_date, is_past, time_params) %}
|
|
{% set MAX_LINES = 3 %}
|
|
{% set state = spot.state.name %}
|
|
|
|
{% if state == 'MINE' %}
|
|
{% set colours = 'bg-primary border-primary-dark text-white' %}
|
|
{% elseif state == 'TAKEN' %}
|
|
{% set colours = 'bg-gray-200 border-gray-400 text-gray-600' %}
|
|
{% elseif state == 'PARTLY' %}
|
|
{% set colours = 'bg-amber-50 border-amber-500 text-amber-900' %}
|
|
{% else %}
|
|
{% set colours = 'bg-emerald-50 border-emerald-500 text-emerald-900' %}
|
|
{% endif %}
|
|
|
|
{% set interactive = not is_past and spot.actionable %}
|
|
{# A supervisor (and so an administrator) may free somebody else's desk -- a colleague calls in
|
|
sick and the spot should go back into the pool. Colours are untouched: MINE still means
|
|
mine, and only clickability widens. Past days stay inert for them too; there is no
|
|
past-write path.
|
|
|
|
Only a card that is not already actionable: a partly booked one still opens the create
|
|
modal, because taking the free window is the more useful act there and a supervisor can
|
|
reach that booking from /manage/bookings anyway. #}
|
|
{% set manageable = not is_past and not interactive and is_granted('ROLE_SUPERVISOR') and spot.bookings is not empty %}
|
|
|
|
{# One line per booking, then the window still free if there is one -- that last line is the
|
|
reason a partly booked card 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" -> "A. Berger" : keeps a card on one line per booking at phone width,
|
|
and the surname tells two Annas apart where the initial would not. #}
|
|
{% set parts = booking.user.name|split(' ')|filter(part => part is not empty) %}
|
|
{% set who = parts|length > 1 ? parts|first|slice(0, 1) ~ '. ' ~ parts|last : 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 %}
|
|
|
|
{# Summarise rather than let one busy desk stretch its whole row. 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 %}
|
|
|
|
{% set tag = interactive or manageable ? 'button' : 'div' %}
|
|
<{{ tag }} data-spot
|
|
{%- if tag == 'button' %} type="button"{% endif %}
|
|
class="flex min-h-[5.5rem] w-full flex-col items-center rounded-md border-2 px-2 py-2 text-center {{ colours }}
|
|
{{- tag == 'button' ? ' cursor-pointer hover:shadow-md focus:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2' : ' cursor-default' }}"
|
|
{%- 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 Booking\CreateController. #}
|
|
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"
|
|
{%- elseif manageable %}
|
|
{# Not app_booking_edit: a card can hold several bookings on one day, and a supervisor
|
|
releases rather than reschedules. The modal lists them and frees one. #}
|
|
hx-get="{{ path('app_manage_spot', { id: spot.resource.id, date: selected_date|date('Y-m-d') }|merge(time_params)) }}"
|
|
hx-target="body"
|
|
hx-swap="beforeend"
|
|
{%- endif %}>
|
|
<span class="text-xs font-semibold{{ lines is empty ? ' my-auto' : '' }}">{{ spot.resource.label }}</span>
|
|
|
|
{% if lines is empty %}
|
|
{# The colour already says "free" to anyone who can see it. #}
|
|
<span class="sr-only">frei</span>
|
|
{% else %}
|
|
<ul class="pt-1 text-[11px] leading-4">
|
|
{% for line in lines %}
|
|
<li>{{ line }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% endif %}
|
|
</{{ tag }}>
|
|
{% endmacro %}
|