fix: allow booking the free part of a partly booked day
This commit is contained in:
1 parent
a88ff7ff24
commit
78839838f0
15 files changed
+541
-60
No files matched your search
@@ -13,6 +13,7 @@
|
||||
<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-amber-50 border-2 border-amber-500"></span> teilweise 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>
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
|
||||
{% for resource in resources %}
|
||||
{% if resource.svgElementId and geometry[resource.svgElementId] is defined %}
|
||||
{{ plan.spot(resource, geometry[resource.svgElementId], 140, 70,
|
||||
bookings_by_resource[resource.id]|default([]), selected_date, is_past, time_params) }}
|
||||
{{ plan.spot(spots[resource.id], geometry[resource.svgElementId], 140, 70,
|
||||
selected_date, is_past, time_params) }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</svg>
|
||||
@@ -18,8 +18,8 @@
|
||||
|
||||
{% for resource in resources %}
|
||||
{% if resource.svgElementId and geometry[resource.svgElementId] is defined %}
|
||||
{{ plan.spot(resource, geometry[resource.svgElementId], 200, 55,
|
||||
bookings_by_resource[resource.id]|default([]), selected_date, is_past, time_params) }}
|
||||
{{ plan.spot(spots[resource.id], geometry[resource.svgElementId], 200, 55,
|
||||
selected_date, is_past, time_params) }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</svg>
|
||||
@@ -1,59 +1,69 @@
|
||||
{#
|
||||
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 free/taken/yours looks identical everywhere.
|
||||
per-type partials carry geometry only and the states look identical everywhere.
|
||||
|
||||
Clicking always opens a modal: the create modal when 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.
|
||||
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.
|
||||
|
||||
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(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 %}
|
||||
{% macro spot(spot, geometry, width, height, selected_date, is_past, time_params) %}
|
||||
{% set state = spot.state.name %}
|
||||
|
||||
{% if mine %}
|
||||
{% if state == 'MINE' %}
|
||||
{% set fill = 'fill-primary' %}{% set stroke = 'stroke-primary-dark' %}{% set text = 'fill-white' %}
|
||||
{% elseif bookings is not empty %}
|
||||
{% 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 (mine or bookings is empty) %}
|
||||
{% set interactive = not is_past and spot.actionable %}
|
||||
|
||||
{# 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 %}
|
||||
{# The second line: what you would be told at a glance. For a partly-free spot that is the
|
||||
window still available, because that is the reason to click it. #}
|
||||
{% if state == 'MINE' %}
|
||||
{% set caption = spot.own.timeRange.allDay ? 'du' : 'du ' ~ spot.own.timeRange %}
|
||||
{% elseif state == 'PARTLY' %}
|
||||
{% set caption = 'frei ' ~ spot.suggested %}
|
||||
{% elseif spot.bookings|length > 1 %}
|
||||
{% set caption = spot.bookings|length ~ ' Buchungen' %}
|
||||
{% elseif spot.bookings is not empty %}
|
||||
{% set caption = spot.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)) }}"
|
||||
{%- 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. #}
|
||||
hx-get="{{ path('app_booking_new', { id: resource.id, date: selected_date|date('Y-m-d') }|merge(time_params)) }}"
|
||||
{# 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 %}>
|
||||
<title>
|
||||
{{- resource.label }}
|
||||
{%- if bookings is empty %} — frei
|
||||
{{- spot.resource.label }}
|
||||
{%- if spot.bookings is empty %} — frei
|
||||
{%- else %}
|
||||
{%- for booking in bookings %} — {{ booking.user.id == app.user.id ? 'du' : booking.user.name }} {{ booking.timeRange }}{% endfor %}
|
||||
{%- for booking in spot.bookings %} — {{ booking.user.id == app.user.id ? 'du' : booking.user.name }} {{ booking.timeRange }}{% endfor %}
|
||||
{%- for window in spot.free %} — frei {{ window }}{% 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>
|
||||
text-anchor="middle" class="{{ text }} text-xs font-semibold">{{ spot.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>
|
||||
|
||||
Reference in new issue
Block a user