{# 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 %} {# 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 admins 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 an admin can reach that booking from /admin/bookings anyway. #} {% set manageable = not is_past and not interactive and is_granted('ROLE_ADMIN') 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" -> "Anna B." : keeps a card on one line per booking at phone width. #} {% 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 %} {# 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 ; 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 an admin releases rather than reschedules. The modal lists them and frees one. #} hx-get="{{ path('app_admin_spot', { id: spot.resource.id, date: selected_date|date('Y-m-d') }|merge(time_params)) }}" hx-target="body" hx-swap="beforeend" {%- endif %}> {{ spot.resource.label }} {% if lines is empty %} {# The colour already says "free" to anyone who can see it. #} frei {% else %} {% endif %} {% endmacro %}