Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add button to close booking in backoffice #88

Merged
merged 27 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f24c7da
create a template link that opens a modal requesting confirmation
Dec 14, 2024
5a34978
added endblock for endpoint
Dec 14, 2024
e5009aa
example usage of modal template
Dec 14, 2024
1ab2b74
moved styling inside the explanation block
Dec 14, 2024
fc4456a
styling
Dec 15, 2024
0dc5058
styling
Dec 15, 2024
add6e98
Add column for close booking
Dec 19, 2024
4cecbd6
add modal for closing a booking
Dec 19, 2024
84d8137
Create modal for closing booking
Dec 19, 2024
6bf9aa4
create Close booking endpoint
Dec 19, 2024
1cd79dc
make close_booking endpoint accessible
Dec 19, 2024
95044a7
formatting
Dec 19, 2024
3f6c03b
Merge branch 'main' into 72-close-booking-backoffice
Dec 19, 2024
8166ec8
moved should_lock to url encoded
Jan 2, 2025
070cbd7
remove unnecessary context
Jan 2, 2025
3747b78
added conditions
Jan 2, 2025
8abf652
fixmes for potential race conditions
Jan 2, 2025
054ce17
switched to early return for close_booking
Jan 2, 2025
0814094
formatting
Jan 2, 2025
aea3e7d
use a bespoke modal not the generic
Jan 3, 2025
560009b
Various minor alterations.
grundleborg Jan 19, 2025
9ded5e2
Merge branch 'master' into 72-close-booking-backoffice
grundleborg Jan 19, 2025
e322425
Fix formatting.
grundleborg Jan 19, 2025
4262cb1
Move messages to better position on the page in backoffice.
grundleborg Jan 19, 2025
e3f6159
Return to the page we were on, not backoffice bookings always.
grundleborg Jan 19, 2025
33bb958
Booking list column changes.
grundleborg Jan 19, 2025
b9d51f8
Fix formatting.
grundleborg Jan 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions backoffice/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from django import forms
from django.forms import ValidationError
from django.urls import reverse_lazy
from django.utils import timezone

from drivers.fields import CustomImageField
Expand Down Expand Up @@ -183,3 +184,10 @@ def save(self, user):
self.driver_profile.approved_to_drive = True
self.driver_profile.approved_by = user
self.driver_profile.save()


class CloseBookingForm(forms.Form):
should_lock = forms.BooleanField(required=False, initial=False)
return_url = forms.CharField(
required=False, initial=reverse_lazy("backoffice_home")
)
4 changes: 4 additions & 0 deletions backoffice/templates/backoffice/bookings/bookings_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
scope="col">
Vehicle
</th>
<th class="border-b border-gray-200 bg-gray-50 py-3 pr-6 text-right text-sm font-semibold text-gray-900"
scope="col">
Actions
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 bg-white">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,7 @@
<td class="whitespace-nowrap px-6 py-3 text-right text-sm text-gray-500">
{{ b.vehicle.registration }}
</td>
<td class="whitespace-nowrap px-6 py-3 text-right text-sm text-gray-500">
{% include "backoffice/bookings/close_booking_modal.html" %}
</td>
</tr>
89 changes: 89 additions & 0 deletions backoffice/templates/backoffice/bookings/close_booking_modal.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
{% if b.reservation_started and b.in_closeable_state %}
<div class="flex-1 flex w-full justify-end" x-data="{lockModalOpen: false}">
<a href="#"
@click="lockModalOpen=true"
class="flex flex-row text-red-600">

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-5 w-5">
<path fill-rule="evenodd"
d="M12 1.5a5.25 5.25 0 00-5.25 5.25v3a3 3 0 00-3 3v6.75a3 3 0 003 3h10.5a3 3 0 003-3v-6.75a3 3 0 00-3-3v-3c0-2.9-2.35-5.25-5.25-5.25zm3.75 8.25v-3a3.75 3.75 0 10-7.5 0v3h7.5z"
clip-rule="evenodd"/>
</svg>

<span class="ml-3">Close</span>
</a>

<div
x-cloak
x-show="lockModalOpen"
class="relative z-10"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
>
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>
<div class="fixed inset-0 z-10 overflow-y-auto">
<div
x-show="lockModalOpen"
class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-data="{ lock: true, url: '{% url "backoffice_close_booking" b.id %}', should_lock:''}"
>
<form action="{% url "backoffice_close_booking" b.id %}" method="GET">
<div
class="relative transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
<div class="sm:flex sm:items-start">
<div
class="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<!-- Heroicon name: outline/exclamation-triangle -->
<svg class="h-6 w-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round"
d="M12 10.5v3.75m-9.303 3.376C1.83 19.126 2.914 21 4.645 21h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 4.88c-.866-1.501-3.032-1.501-3.898 0L2.697 17.626zM12 17.25h.007v.008H12v-.008z"/>
</svg>
</div>
<div class="mt-2 ml-4 text-left">
<h3 class="text-lg font-medium leading-6 text-gray-900" id="modal-title">Close Booking</h3>
<div class="mt-4">
<p class="text-sm text-gray-500">Are you sure you wish to close this booking?</p>
<p class="py-2 text-sm text-gray-500">
<input type="checkbox" id="should_lock{{ b.id }}" name="should_lock">
<input type="hidden" name="return_url" value="{{ request.get_full_path }}"/>
<label for="should_lock{{ b.id }}">Lock Vehicle</label>
</p>
</div>
</div>
</div>
<div class="mt-4 sm:flex sm:flex-row-reverse">
<button
type="submit"
class="inline-flex w-full justify-center rounded-md border border-transparent bg-red-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 sm:ml-3 sm:w-auto sm:text-sm"
>
Close Booking
</button>
<button
type="button"
class="mt-3 inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:mt-0 sm:w-auto sm:text-sm"
@click="lockModalOpen=false"
>
Cancel
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% endif %}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
scope="col">
Vehicle
</th>
<th class="border-b border-gray-200 bg-gray-50 py-3 pr-6 text-right text-sm font-semibold text-gray-900"
scope="col">
Actions
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 bg-white">
Expand Down
3 changes: 2 additions & 1 deletion backoffice/templates/backoffice_base.html
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,6 @@
</button>
</div>
<main class="flex-1">
{% include "components/messages.html" %}
<div class="border-b border-gray-200 px-4 py-4 sm:flex sm:items-center sm:justify-between sm:px-6 lg:px-8">
<div class="min-w-0 flex-1">
<h1 class="text-lg font-medium leading-6 text-gray-900 sm:truncate">{% block heading %}{% endblock %}</h1>
Expand All @@ -213,6 +212,8 @@ <h1 class="text-lg font-medium leading-6 text-gray-900 sm:truncate">{% block hea
</div>
</div>

{% include "components/messages.html" %}

{% block main %} {% endblock %}
</main>
</div>
Expand Down
5 changes: 5 additions & 0 deletions backoffice/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
views.reject_billing_account,
name="backoffice_reject_billing_account",
),
path(
"bookings/close/<int:booking_id>",
views.close_booking,
name="backoffice_close_booking",
),
path(
"lock/<int:id>/",
views.lock,
Expand Down
55 changes: 54 additions & 1 deletion backoffice/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@
get_all_pending_approval as get_all_driver_profiles_pending_approval,
DriverProfile,
)
from hardware.models import Vehicle, Box, BoxAction
from hardware.models import Vehicle, BoxAction
from users.models import User

from .decorators import require_backoffice_access
from .forms import DriverProfileApprovalForm, DriverProfileReviewForm
from .forms import DriverProfileApprovalForm, DriverProfileReviewForm, CloseBookingForm


@require_backoffice_access
Expand Down Expand Up @@ -317,6 +318,58 @@ def vehicles(request):
return render(request, "backoffice/vehicles.html", context)


@require_backoffice_access
def close_booking(request, booking_id):
booking = Booking.objects.get(pk=booking_id)

form = CloseBookingForm(request.GET or None)
if form.is_valid():
should_lock = form.cleaned_data.get("should_lock")
return_url = form.cleaned_data.get("return_url")
else:
should_lock = False
return_url = reverse("backoffice_home")

# Don't allow closing the booking if it hasn't started yet.
if not booking.reservation_started():
message = f"Failed to close booking #{booking_id} as it hasn't started yet."
messages.error(request, message)
return redirect(return_url)

# FIXME: potential race condition as booking state could have been changed elsewhere
if not booking.in_closeable_state():
message = f"Failed to close booking #{booking_id} as it is in an inappropriate state: {booking.get_state_display()}."
messages.error(request, message)
return redirect(return_url)

booking.state = Booking.STATE_INACTIVE
booking.save()

box = Box.objects.get(pk=booking.vehicle.box.id)
# FIXME: May also be a race condition
if box.current_booking == booking_id:
box.current_booking = None
box.save()

message = f"Booking #{booking_id} closed."
messages.success(request, message)

if should_lock:
time_to_expire = timezone.now() + timezone.timedelta(minutes=10)
action = BoxAction(
action="lock",
created_at=timezone.now(),
expires_at=time_to_expire,
box=box,
user_id=request.user.id,
)
action.save()
message = f"Lock action sent to vehicle {box.vehicle.registration}."
messages.success(request, message)

return redirect(return_url)


@require_backoffice_access
def lock(request, id):
vehicle = Vehicle.objects.get(pk=id)
Expand Down
17 changes: 17 additions & 0 deletions bookings/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,23 @@ def reservation_in_progress(self):
now = timezone.now()
return self.reservation_time.lower <= now <= self.reservation_time.upper

def reservation_started(self):
"""Returns True if the booking has passed its start time, otherwise False."""
return timezone.now() >= self.reservation_time.lower

def in_closeable_state(self):
"""
Return True if the state the booking is in allows it to be closed, else False.
Only considers the booking state. This doesn't consider who is trying to close it,
or the current time in relation to the booking time.
"""
return self.state in {
Booking.STATE_ACTIVE,
Booking.STATE_PENDING,
Booking.STATE_LATE,
Booking.STATE_INACTIVE,
}

@property
def cancelled(self):
return self.state == self.STATE_CANCELLED
Expand Down
77 changes: 77 additions & 0 deletions theme/templates/components/modal.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<div class="flex-1 flex w-full" x-data="{lockModalOpen: false}">
<a href="#"
@click="lockModalOpen=true"
class="{% block style %}relative -mr-px w-0 flex-1 inline-flex items-center justify-center py-4 text-sm text-gray-700 font-medium border border-transparent rounded-br-lg hover:bg-gray-300{% endblock %}">

{% block svg %}{% endblock %}

<span class="ml-3">{% block button_text %}Click{% endblock %}</span>
</a>

<div
x-cloak
x-show="lockModalOpen"
class="relative z-10"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
>
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>
<div class="fixed inset-0 z-10 overflow-y-auto">
<div
x-show="lockModalOpen"
class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<div
class="relative transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
<div class="sm:flex sm:items-start">
<div
class="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<!-- Heroicon name: outline/exclamation-triangle -->
<svg class="h-6 w-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round"
d="M12 10.5v3.75m-9.303 3.376C1.83 19.126 2.914 21 4.645 21h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 4.88c-.866-1.501-3.032-1.501-3.898 0L2.697 17.626zM12 17.25h.007v.008H12v-.008z"/>
</svg>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg font-medium leading-6 text-gray-900" id="modal-title">{% block title %}Confirm your choice{% endblock %}</h3>
<div class="mt-2">
{% block explanation %}<p class="text-sm text-gray-500">Are you sure you wish to proceed?</p>{% endblock %}
</div>
</div>
</div>
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<a
href="{% block endpoint %}{% endblock %}"
type="button"
class="inline-flex w-full justify-center rounded-md border border-transparent bg-red-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 sm:ml-3 sm:w-auto sm:text-sm"
>
{% block confirm %}Confirm{% endblock %}
</a>
<button
type="button"
class="mt-3 inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:mt-0 sm:w-auto sm:text-sm"
@click="lockModalOpen=false"
>
Cancel
</button>
</div>
</div>
</div>
</div>
</div>
</div>

23 changes: 23 additions & 0 deletions theme/templates/components/modal_example.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{% extends "components/modal.html" %}

{%block style %}relative -mr-px w-0 flex-1 inline-flex items-center justify-center py-4 text-sm text-gray-700 font-medium border border-transparent rounded-br-lg hover:bg-gray-300{% endblock %}

{%block svg %}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-5 w-5">
<path fill-rule="evenodd"
d="M12 1.5a5.25 5.25 0 00-5.25 5.25v3a3 3 0 00-3 3v6.75a3 3 0 003 3h10.5a3 3 0 003-3v-6.75a3 3 0 00-3-3v-3c0-2.9-2.35-5.25-5.25-5.25zm3.75 8.25v-3a3.75 3.75 0 10-7.5 0v3h7.5z"
clip-rule="evenodd"/>
</svg>
{% endblock %}

{%block button_text %}Lock{% endblock %}

{%block title %}Lock Vehicle{% endblock %}

{% block explanation %}
<p class="text-sm text-gray-500">Are you sure you wish to lock that vehicle?</p>
{% endblock %}

{% block endpoint %} {% url "backoffice_vehicles" %} {% endblock %}

{%block confirm %}Lock Car{% endblock %}
Loading