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

Back office User page including RFID card management #93

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
11 changes: 11 additions & 0 deletions backoffice/templates/backoffice/components/card_card.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<div class="w-72 border shadow rounded-lg h-36 flex flex-col gap-3 items-center justify-center {% if card.enabled %}bg-green-400{% else %}bg-red-500{% endif %} {% if card.operator %}border-red-700 border-4{% endif %}">
<p>Card id: {{ card.id }}</p>
<p>
{% if card.enabled %}
ENABLED
{% else %}
DISABLED
{% endif %}
</p>
{% if card.operator %}<p>OPERATOR</p>{% endif %}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<div class="rounded-lg shadow w-96 px-3 py-3">
<h2 class="text-xl font-bold text-gray-800 mb-4">{{ profile.full_name }}</h2>

<p class="text-gray-600 mb-2">
<strong>Created At:</strong> {{ profile.created_at }}
</p>
<p class="text-gray-600 mb-2">
<strong>Updated At:</strong> {{ profile.updated_at }}
</p>
<p class="text-gray-600 mb-2">
<strong>Submitted At:</strong> {{ profile.submitted_at }}
</p>
<p class="text-gray-600 mb-2">
<strong>Approved At:</strong> {{ profile.approved_at }}
</p>
<p class="text-gray-600 mb-2">
<strong>Expires At:</strong> {{ profile.expires_at }}
</p>

<div class="mt-4">
<h3 class="font-semibold text-gray-700 mb-2">Driver Details</h3>
<p><strong>Licence Number:</strong> {{ profile.licence_number }}</p>
<p><strong>Issue Date:</strong> {{ profile.licence_issue_date }}</p>
<p><strong>Expiry Date:</strong> {{ profile.licence_expiry_date }}</p>
</div>

<div class="mt-4">
<h3 class="font-semibold text-gray-700 mb-2">Address</h3>
<p>{{ profile.address_line_1 }}</p>
{% if profile.address_line_2 %}<p>{{ profile.address_line_2 }}</p>{% endif %}
{% if profile.address_line_3 %}<p>{{ profile.address_line_3 }}</p>{% endif %}
<p>{{ profile.postcode }}</p>
</div>

<div class="mt-4 grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<h3 class="font-semibold text-gray-700 mb-2">Licence Images</h3>
<p><strong>Front:</strong> <a href="/media/{{ profile.licence_front }}" target="_blank" class="text-blue-500">View</a></p>
<p><strong>Back:</strong> <a href="/media/{{ profile.licence_back }}" target="_blank" class="text-blue-500">View</a></p>
<p><strong>Selfie:</strong> <a href="/media/{{ profile.licence_selfie }}" target="_blank" class="text-blue-500">View</a></p>
</div>
<div>
<h3 class="font-semibold text-gray-700 mb-2">Proof of Address</h3>
<p><strong>Document:</strong> <a href="/media/{{ profile.proof_of_address }}" target="_blank" class="text-blue-500">View</a></p>
</div>
</div>

<div class="mt-4">
<h3 class="font-semibold text-gray-700 mb-2">Approvals</h3>
<ul class="list-inside">
<li class="{% if profile.approved_full_name %}text-green-700 checked{% else %}text-red-800 crossed{% endif %} ">
Full Name</li>
<li class="{% if profile.approved_address %}text-green-700 checked{% else %}text-red-800 crossed{% endif %} ">
Address</li>
<li class="{% if profile.approved_date_of_birth %}text-green-700 checked{% else %}text-red-800 crossed{% endif %} ">
Date of Birth</li>
<li class="{% if profile.approved_licence_number %}text-green-700 checked{% else %}text-red-800 crossed{% endif %} ">
Licence Number</li>
<li class="{% if profile.approved_proof_of_address %}text-green-700 checked{% else %}text-red-800 crossed{% endif %} ">
Proof of Address</li>
</ul>
</div>
</div>
24 changes: 24 additions & 0 deletions backoffice/templates/backoffice/components/user_details.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<div class="max-w-3xl shadow rounded-lg p-6">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<h3 class="font-semibold text-gray-700 mb-2">Basic Information</h3>
<p class="text-gray-600"><strong>First Name:</strong> {{ user_details.first_name }}</p>
<p class="text-gray-600"><strong>Last Name:</strong> {{ user_details.last_name }}</p>
<p class="text-gray-600"><strong>Email:</strong> {{ user_details.email }}</p>
{% if user_details.mobile %}
<p class="text-gray-600"><strong>Mobile:</strong> {{ user_details.mobile }}</p>
{% elif user_details.pending_mobile %}
<p class="text-gray-600"><strong>Pending Mobile:</strong> {{ user_details.pending_mobile }}</p>
{% endif %}
</div>
<div>
<h3 class="font-semibold text-gray-700 mb-2">Account Information</h3>
<p class="text-gray-600"><strong>Date Joined:</strong> {{ user_details.date_joined }}</p>
<p class="text-gray-600"><strong>Last Login:</strong> {{ user_details.last_login }}</p>
<p class="text-gray-600"><strong>Is Active:</strong> {{ user_details.is_active }}</p>
<p class="text-gray-600"><strong>Is Staff:</strong> {{ user_details.is_staff }}</p>
<p class="text-gray-600"><strong>Is Operator:</strong> {{ user_details.is_operator }}</p>
</div>
</div>

</div>
65 changes: 65 additions & 0 deletions backoffice/templates/backoffice/user_details.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{% extends "backoffice_base.html" %}

{% block heading %}<div class="font-bold">User - {{ user_details.username }} ({{ user_details.first_name|title|add:" "|add:user_details.last_name|title }})</div>{% endblock %}


{% block main %}
<div>
<h3 class="text-lg font-bold px-3 py-2">Bookings</h3>
<div class="px-5">
<div class="inline-block min-w-full border-b border-gray-200 align-middle">
{% include "backoffice/bookings/bookings_list.html" with bookings=page %}
</div>

{% include "backoffice/components/paginator.html" with page=page page_range=page_range %}
</div>
</div>
<div class="px-5 py-2">
{% include "backoffice/components/user_details.html" with user_details=user_details%}
</div>
<div>
<h3 class="text-lg font-bold px-3 py-2">Driver Profiles</h3>
<div class="flex flex-wrap gap-5 px-5">
{% for p in driver_profiles %}
{% include "backoffice/components/driver_profile_card.html" with profile=p %}
{% endfor %}
</div>
</div>
<div>
<h3 class="text-lg font-bold px-3 py-2">Owned Billing Accounts</h3>
<div class="flex flex-wrap gap-5 px-5">
{% for ba in owned_billing_accounts %}
<div class="w-96">
{% include "billing/components/billing_account_card.html" with ba=ba owner=user_details.first_name %}
</div>
{% endfor %}
</div>
</div>
<div>
<h3 class="text-lg font-bold px-3 py-2">Member of Billing Accounts</h3>
<div class="flex flex-wrap gap-5 px-5">
{% for ba in member_billing_accounts %}
<div class="w-96">
{% include "billing/components/billing_account_card.html" with ba=ba owner=ba.owner %}
</div>
{% endfor %}
</div>
</div>
<div class="pb-10">
<h3 class="text-lg font-bold px-3 py-2">Cards</h3>
<div class="flex flex-wrap gap-5 px-5">
{% for c in cards %}
{% include "backoffice/components/card_card.html" with card=c %}
{% endfor %}
<a class="w-72 h-36 border-gray-600 hover:border-gray-800 shadow hover:shadow-lg bg-slate-400 hover:bg-slate-300 border-4 rounded-lg box-border"
href="{% url "backoffice_add_card" user_details.id %}"
>
<div class="w-full h-full flex flex-col gap-3 items-center justify-center ">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="#4b5563" class="size-20">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v6m3-3H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
</div>
</a>
</div>
</div>
{% endblock %}
19 changes: 19 additions & 0 deletions backoffice/templates/backoffice/users/add_card.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% extends "backoffice_base.html" %}

{% load crispy_forms_tags %}

{% block heading %}Add card for {{user_details.username|title}} {% endblock %}
{% block buttons %}

{% endblock %}

{% block main %}
<div class="w-96 p-5">
<form method="POST" class="px-5 py-3" action="{% url "backoffice_add_card" user_details.id %}">
{% csrf_token %}
{{ form|crispy }}
<input type="submit" value="Add Card" class="w-72 flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-teal-600 hover:bg-teal-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"/>
</form>
</div>

{% endblock %}
13 changes: 8 additions & 5 deletions backoffice/templates/backoffice/users/users_list_item.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
{% load user_utils %}

<tr>

<td class="px-6 py-3 text-sm font-medium text-gray-500">
{{ u.first_name }} {{ u.last_name }}
<a href="{% url "backoffice_user_with_name" u.username %}" class="underline">
{% if u.first_name %}{{ u.first_name }} {{ u.last_name }}{% else %}{{ u.username }}{% endif %}
</a>
</td>
<td class="whitespace-nowrap px-6 py-3 text-sm text-gray-500">
{{ u.email }}
Expand All @@ -14,12 +17,12 @@
{% can_drive_full u as full %}
{% if full %}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
class="w-6 h-6 text-green-600">
class="w-6 h-6 text-green-600">
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5"/>
</svg>
{% else %}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
class="w-6 h-6 text-red-600">
class="w-6 h-6 text-red-600">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
</svg>
{% endif %}
Expand All @@ -28,12 +31,12 @@
{% can_drive_external u as external %}
{% if external %}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
class="w-6 h-6 text-green-600">
class="w-6 h-6 text-green-600">
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5"/>
</svg>
{% else %}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
class="w-6 h-6 text-red-600">
class="w-6 h-6 text-red-600">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
</svg>
{% endif %}
Expand Down
5 changes: 5 additions & 0 deletions backoffice/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
path("", views.home, name="backoffice_home"),
path("bookings/", views.bookings, name="backoffice_bookings"),
path("users/", views.users, name="backoffice_users"),
path("users/card/add/<int:id>/", views.add_card, name="backoffice_add_card"),
path("users/<int:id>/", views.user_details, name="backoffice_user_details"),
path(
"users/<str:username>/", views.user_with_name, name="backoffice_user_with_name"
),
path("accounts/", views.accounts, name="backoffice_accounts"),
path("approvals/", views.approvals, name="backoffice_approvals"),
path("vehicles/", views.vehicles, name="backoffice_vehicles"),
Expand Down
111 changes: 108 additions & 3 deletions backoffice/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@
from crispy_forms.layout import Layout
from crispy_forms.bootstrap import InlineField

from itertools import chain

from django.conf import settings
from django.contrib.postgres.fields import RangeBoundary
from django.contrib import messages
from django.core.mail import EmailMessage
from django.core.paginator import Paginator
from django.db.models import Q
from django.forms import ModelChoiceField
from django.shortcuts import redirect, render
from django.shortcuts import redirect, render, get_object_or_404
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils import timezone
from django.core.exceptions import ObjectDoesNotExist

from django_filters import FilterSet, ModelChoiceFilter

Expand All @@ -22,13 +25,14 @@
get_all_pending_approval as get_all_billing_accounts_pending_approval,
BillingAccount,
)
from billing.forms import UpdatePurchaseOrderForm
from bookings.models import Booking
from drivers.models import (
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 hardware.models import Vehicle, Card, Box, BoxAction
from hardware.forms import CreateCard
from users.models import User

from .decorators import require_backoffice_access
Expand Down Expand Up @@ -402,3 +406,104 @@ def perform_box_action(request, vehicle, action_to_perform, user):
# FIXME: message is dispatched regardless of outcome - may be worth exploring options to send different messages
message = f"{user.username} has {action_to_perform}ed vehicle {vehicle.name} ({vehicle.registration})"
messages.success(request, message)


@require_backoffice_access
def user_details(request, id):
selected_user_details = User.objects.get(pk=id)
selected_user_driver_profiles = DriverProfile.objects.filter(user__id=id)
selected_user_owned_billing_accounts = BillingAccount.objects.filter(owner__id=id)
selected_user_member_billing_accounts = BillingAccount.objects.filter(members=id)
selected_user_cards = Card.objects.filter(user__id=id)
selected_user_bookings = Booking.objects.filter(user__id=id).order_by(
"-reservation_time"
)

filter = BookingsFilter(request.GET, queryset=selected_user_bookings)
bookings = filter.qs
paginator = Paginator(bookings, 5)

page_number = request.GET.get("page", 1)
page_obj = paginator.get_page(page_number)
page_range = paginator.get_elided_page_range(
number=page_number, on_each_side=1, on_ends=1
)

# Process and add purchase order update forms
update_success = False
ba_id = None
if request.method == "POST":
form = UpdatePurchaseOrderForm(request.POST)
if form.is_valid():
ba_id = form.cleaned_data["ba_id"]
updated_billing_account = get_object_or_404(BillingAccount, id=ba_id)
# backoffice user is allowed to edit any account
update_purchase_order_form = UpdatePurchaseOrderForm(
request.POST,
instance=updated_billing_account,
)
update_purchase_order_form.save()
update_success = True
else:
ba_id = form.cleaned_data["ba_id"]

for billing_account in chain(
selected_user_owned_billing_accounts, selected_user_member_billing_accounts
):
if billing_account.id == ba_id:
billing_account.purchase_order_update_form = form
billing_account.successfully_updated = update_success
else:
billing_account.purchase_order_update_form = UpdatePurchaseOrderForm(
initial={
"ba_id": billing_account.id,
"business_purchase_order": billing_account.business_purchase_order,
}
)
billing_account.successfully_updated = False

# Hack for pagination & filtering. Should really use a templatetag to generate URLs.
_request_copy = request.GET.copy()
parameters = _request_copy.pop("page", True) and _request_copy.urlencode()
context = {
"menu": "user",
"user": request.user,
"user_details": selected_user_details,
"driver_profiles": selected_user_driver_profiles,
"owned_billing_accounts": selected_user_owned_billing_accounts,
"member_billing_accounts": selected_user_member_billing_accounts,
"cards": selected_user_cards,
"page": page_obj,
"page_range": page_range,
"filter": filter,
"parameters": parameters,
}
return render(request, "backoffice/user_details.html", context)


@require_backoffice_access
def user_with_name(request, username):
user = User.objects.get(username=username)
return user_details(request, user.id)


@require_backoffice_access
def add_card(request, id):
try:
selected_user_details = User.objects.get(pk=id)
except ObjectDoesNotExist:
messages.error(request, "No such user.")
return redirect("backoffice_users")
if request.method == "POST":
form = CreateCard(request.POST)
if form.is_valid():
form.save()
return redirect("backoffice_user_details", id=id)
else:
form = CreateCard(initial={"user": id})
context = {
"user": request.user,
"form": form,
"user_details": selected_user_details,
}
return render(request, "backoffice/users/add_card.html", context)
Loading
Loading