diff --git a/src/genlab_bestilling/models.py b/src/genlab_bestilling/models.py index 5a86aa2f..4a0e16b5 100644 --- a/src/genlab_bestilling/models.py +++ b/src/genlab_bestilling/models.py @@ -390,6 +390,9 @@ def get_absolute_url(self) -> str: kwargs={"pk": self.pk, "genrequest_id": self.genrequest_id}, ) + def get_absolute_staff_url(self) -> str: + return reverse("staff:order-equipment-detail", kwargs={"pk": self.pk}) + def confirm_order(self) -> Any: if not EquimentOrderQuantity.objects.filter(order=self).exists(): raise Order.CannotConfirm(_("No equipments found")) @@ -428,6 +431,9 @@ def get_absolute_url(self) -> str: kwargs={"pk": self.pk, "genrequest_id": self.genrequest_id}, ) + def get_absolute_staff_url(self) -> str: + return reverse("staff:order-extraction-detail", kwargs={"pk": self.pk}) + def clone(self) -> None: """ Generates a clone of the model, with a different ID @@ -528,6 +534,9 @@ def get_absolute_url(self) -> str: kwargs={"pk": self.pk, "genrequest_id": self.genrequest_id}, ) + def get_absolute_staff_url(self) -> str: + return reverse("staff:order-analysis-detail", kwargs={"pk": self.pk}) + def get_type(self) -> str: return "analysis" diff --git a/src/staff/tables.py b/src/staff/tables.py index ebb149fd..5ea38a30 100644 --- a/src/staff/tables.py +++ b/src/staff/tables.py @@ -10,6 +10,7 @@ EquipmentOrder, ExtractionOrder, ExtractionPlate, + Order, Sample, SampleMarkerAnalysis, ) @@ -300,3 +301,139 @@ class Meta: attrs = {"class": "w-full table-auto tailwind-table table-sm"} empty_text = "No Plates" + + +FLAG_OUTLINE = "" +FLAG_FILLED = "" +URGENT_FILLED = ( + "" +) + + +class StatusMixinTable(tables.Table): + status = tables.Column( + orderable=False, + verbose_name="Status", + ) + + def render_status(self, value: Order.OrderStatus, record: Order) -> str: + status_colors = { + "Processing": "bg-yellow-100 text-yellow-800", + "Completed": "bg-green-100 text-green-800", + "Delivered": "bg-red-100 text-red-800", + } + status_text = { + "Processing": "Processing", + "Completed": "Completed", + "Delivered": "Not started", + } + color_class = status_colors.get(value, "bg-gray-100 text-gray-800") + status_text = status_text.get(value, "Unknown") + return mark_safe( # noqa: S308 + f'{status_text}' # noqa: E501 + ) + + +class StaffIDMixinTable(tables.Table): + id = tables.Column( + orderable=False, + empty_values=(), + ) + + def render_id( + self, record: ExtractionOrder | AnalysisOrder | EquipmentOrder + ) -> str: + url = record.get_absolute_staff_url() + + return mark_safe(f'{record}') # noqa: S308 + + +class UrgentOrderTable(StaffIDMixinTable, StatusMixinTable): + description = tables.Column( + accessor="genrequest__name", + verbose_name="Description", + orderable=False, + ) + + delivery_date = tables.Column( + accessor="genrequest__expected_samples_delivery_date", + verbose_name="Delivery date", + orderable=False, + ) + + def render_delivery_date(self, value: Any) -> str: + if value: + return value.strftime("%d/%m/%Y") + return "-" + + class Meta: + model = Order + fields = ["id", "description", "delivery_date", "status"] + empty_text = "No urgent orders" + template_name = "django_tables2/tailwind_inner.html" + + +class NewOrderTable(StaffIDMixinTable): + description = tables.Column( + accessor="genrequest__name", + verbose_name="Description", + orderable=False, + ) + + delivery_date = tables.Column( + accessor="genrequest__expected_samples_delivery_date", + verbose_name="Delivery date", + orderable=False, + ) + + def render_delivery_date(self, value: Any) -> str: + if value: + return value.strftime("%d/%m/%Y") + return "-" + + samples = tables.Column( + accessor="sample_count", + verbose_name="Samples", + orderable=False, + ) + + def render_samples(self, value: int) -> str: + if value > 0: + return str(value) + return "-" + + class Meta: + model = Order + fields = ["id", "description", "delivery_date", "samples"] + empty_text = "No new orders" + template_name = "django_tables2/tailwind_inner.html" + + +class AssignedOrderTable(StatusMixinTable, StaffIDMixinTable): + priority = tables.Column( + orderable=False, + verbose_name="Priority", + accessor="is_urgent", + ) + + def render_priority(self, value: bool) -> str: + if value: + return mark_safe(URGENT_FILLED) # noqa: S308 + return "" + + samples_completed = tables.Column( + accessor="sample_count", + verbose_name="Samples completed", + orderable=False, + ) + + def render_samples_completed(self, value: int) -> str: + if value > 0: + return "- / " + str(value) + return "-" + + class Meta: + model = Order + fields = ["priority", "id", "samples_completed", "status"] + empty_text = "No assigned orders" + template_name = "django_tables2/tailwind_inner.html" diff --git a/src/staff/templates/staff/components/order_table.html b/src/staff/templates/staff/components/order_table.html new file mode 100644 index 00000000..2fb0fc1b --- /dev/null +++ b/src/staff/templates/staff/components/order_table.html @@ -0,0 +1,6 @@ +{% load django_tables2 %} + +
+

{{ title }} ({{ count }})

+ {% render_table table %} +
diff --git a/src/staff/templates/staff/dashboard.html b/src/staff/templates/staff/dashboard.html index 46797bc1..6ff9aaf4 100644 --- a/src/staff/templates/staff/dashboard.html +++ b/src/staff/templates/staff/dashboard.html @@ -1,14 +1,19 @@ {% extends 'staff/base.html' %} + {% load i18n %} {% load static %} {% load tz %} - -{% block head_javascript %} - -{% endblock %} +{% load order_tags %} {% block content %}
+
+ All + {% for area in areas %} + {{ area.name }} + {% endfor %} +
+

{{ now|date:'F j, Y' }} | @@ -16,67 +21,14 @@

- {% if delivered_orders|length > 0 %} -
-

Delivered Orders

- - {% for order in delivered_orders %} - {% if order.polymorphic_ctype.model == 'analysisorder' %} -

{{ order }} - {{ order.name }}

- {% elif order.polymorphic_ctype.model == 'equipmentorder' %} -

{{ order }} - {{ order.name }}

- {% elif order.polymorphic_ctype.model == 'extractionorder' %} -

{{ order }} - {{ order.name }}

- {% else %} -

{{ order }} - {{ order.name }}

- {% endif %} - {% endfor %} +
+
+ {% urgent_orders_table area=area %} + {% new_orders_table area=area %}
- {% endif %} - - {% if urgent_orders|length > 0 %} -
-

Urgent orders

- {% for order in urgent_orders %} - {% if order.polymorphic_ctype.model == 'analysisorder' %} -

{{ order }} - {{ order.name }} - Deadline: {{ order.expected_delivery_date|default:'-' }} - Status: {{ order.status|default:'-' }}

- {% elif order.polymorphic_ctype.model == 'equipmentorder' %} -

{{ order }} - {{ order.name }} - Deadline: {{ order.expected_delivery_date|default:'-' }} - Status: {{ order.status|default:'-' }}

- {% elif order.polymorphic_ctype.model == 'extractionorder' %} -

{{ order }} - {{ order.name }} - Deadline: {{ order.expected_delivery_date|default:'-' }} - Status: {{ order.status|default:'-' }}

- {% else %} -

{{ order }} - {{ order.name }}

- {% endif %} - {% endfor %} -
- {% else %} -
-

Urgent orders

-

No urgent orders found.

-
- {% endif %} - - {% if assigned_orders|length > 0 %} -
-

My orders ({{ assigned_orders|length }})

- - {% for order in assigned_orders %} - {% if order.polymorphic_ctype.model == 'analysisorder' %} -

{{ order }} - {{ order.name }}

- {% elif order.polymorphic_ctype.model == 'equipmentorder' %} -

{{ order }} - {{ order.name }}

- {% elif order.polymorphic_ctype.model == 'extractionorder' %} -

{{ order }} - {{ order.name }}

- {% else %} -

{{ order }} - {{ order.name }}

- {% endif %} - {% endfor %} +
+ {% assigned_orders_table %}
- {% else %} -
-

Assigned orders

-

No assigned orders found.

-
- {% endif %} +
{% endblock %} diff --git a/src/staff/templatetags/__init__.py b/src/staff/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/staff/templatetags/order_tags.py b/src/staff/templatetags/order_tags.py new file mode 100644 index 00000000..766c190d --- /dev/null +++ b/src/staff/templatetags/order_tags.py @@ -0,0 +1,107 @@ +from django import template +from django.db import models + +from genlab_bestilling.models import Area, Order + +from ..tables import AssignedOrderTable, NewOrderTable, UrgentOrderTable + +register = template.Library() + + +@register.inclusion_tag("staff/components/order_table.html", takes_context=True) +def urgent_orders_table(context: dict, area: Area | None = None) -> dict: + urgent_orders = Order.objects.filter( + is_urgent=True, + status__in=[Order.OrderStatus.PROCESSING, Order.OrderStatus.DELIVERED], + ).select_related("genrequest") + + if area: + urgent_orders = urgent_orders.filter(genrequest__area=area) + + urgent_orders = urgent_orders.only( + "id", "genrequest__name", "genrequest__expected_samples_delivery_date", "status" + ).order_by( + models.Case( + models.When(status=Order.OrderStatus.PROCESSING, then=0), + models.When(status=Order.OrderStatus.DELIVERED, then=1), + models.When(status=Order.OrderStatus.COMPLETED, then=2), + default=3, + output_field=models.IntegerField(), + ), + "-created_at", + ) + + return { + "title": "Urgent orders", + "table": UrgentOrderTable(urgent_orders), + "count": urgent_orders.count(), + "request": context.get("request"), + } + + +@register.inclusion_tag("staff/components/order_table.html", takes_context=True) +def new_orders_table(context: dict, area: Area | None = None) -> dict: + new_orders = ( + Order.objects.filter(status=Order.OrderStatus.DELIVERED) + .select_related("genrequest") + .annotate(sample_count=models.Count("extractionorder__samples")) + .only( + "id", + "genrequest__name", + "genrequest__expected_samples_delivery_date", + "status", + ) + .order_by("status") + ) + + if area: + new_orders = new_orders.filter(genrequest__area=area) + + new_orders = new_orders.order_by("-created_at") + + return { + "title": "New orders", + "table": NewOrderTable(new_orders), + "count": new_orders.count(), + "request": context.get("request"), + } + + +@register.inclusion_tag("staff/components/order_table.html", takes_context=True) +def assigned_orders_table(context: dict) -> dict: + assigned_orders = ( + Order.objects.filter( + status__in=[ + Order.OrderStatus.PROCESSING, + Order.OrderStatus.DELIVERED, + Order.OrderStatus.COMPLETED, + ] + ) + .select_related("genrequest") + .annotate( + sample_count=models.Count("extractionorder__samples"), + ) + .only( + "id", + "genrequest__name", + "genrequest__expected_samples_delivery_date", + "status", + ) + .order_by( + models.Case( + models.When(status=Order.OrderStatus.PROCESSING, then=0), + models.When(status=Order.OrderStatus.DELIVERED, then=1), + models.When(status=Order.OrderStatus.COMPLETED, then=2), + default=3, + output_field=models.IntegerField(), + ), + "-created_at", + ) + ) + + return { + "title": "My orders", + "table": AssignedOrderTable(assigned_orders), + "count": assigned_orders.count(), + "request": context.get("request"), + } diff --git a/src/staff/views.py b/src/staff/views.py index 23573f0e..956c576f 100644 --- a/src/staff/views.py +++ b/src/staff/views.py @@ -18,6 +18,7 @@ from genlab_bestilling.models import ( AnalysisOrder, + Area, EquipmentOrder, ExtractionOrder, ExtractionPlate, @@ -67,22 +68,25 @@ def test_func(self) -> bool: class DashboardView(StaffMixin, TemplateView): template_name = "staff/dashboard.html" - def get_context_data(self, **kwargs) -> dict[str, Any]: - context = super().get_context_data(**kwargs) + def get_area_from_query(self) -> Area | None: + area_id = self.request.GET.get("area") + if area_id: + try: + return Area.objects.get(pk=area_id) + except Area.DoesNotExist: + return None + return None - urgent_orders = Order.objects.filter( - is_urgent=True, - status__in=[Order.OrderStatus.PROCESSING, Order.OrderStatus.DELIVERED], - ).order_by("-created_at") - context["urgent_orders"] = urgent_orders + def get_areas(self) -> models.QuerySet[Area]: + return Area.objects.all().order_by("name") - delivered_orders = Order.objects.filter(status=Order.OrderStatus.DELIVERED) + def get_context_data(self, **kwargs) -> dict[str, Any]: + context = super().get_context_data(**kwargs) - context["delivered_orders"] = delivered_orders - context["assigned_orders"] = self.request.user.responsible_orders.filter( - status__in=[Order.OrderStatus.DELIVERED, Order.OrderStatus.PROCESSING] - ).order_by("-created_at") context["now"] = now() + context["areas"] = self.get_areas() + context["area"] = self.get_area_from_query() + return context diff --git a/src/templates/django_tables2/tailwind.html b/src/templates/django_tables2/tailwind.html index 57650799..a322ecb5 100644 --- a/src/templates/django_tables2/tailwind.html +++ b/src/templates/django_tables2/tailwind.html @@ -1,126 +1,7 @@ -{% load django_tables2 %} -{% load i18n %} {% block table-wrapper %} -
-
- {% block table %} - - {% block table.thead %} - {% if table.show_header %} - - - {% for column in table.columns %} - - {% endfor %} - - - {% endif %} - {% endblock table.thead %} - - - {% block table.tbody %} - - {% for row in table.paginated_rows %} - {% block table.tbody.row %} - - - {% for column, cell in row.items %} - - {% endfor %} - - {% endblock table.tbody.row %} - {% empty %} - {% if table.empty_text %} - {% block table.tbody.empty_text %} - - {% endblock table.tbody.empty_text %} - {% endif %} - {% endfor %} - - {% endblock table.tbody %} - - - {% block table.tfoot %} - {% if table.has_footer %} - - - {% for column in table.columns %} - - {% endfor %} - - - {% endif %} - {% endblock table.tfoot %} -
- {% if column.orderable %} - {% comment%} - If the column is orderable, two small arrows will show next to the column name to signal that it can be sorted. - {% endcomment%} - - {{ column.header }} - - - {% else %} - {{ column.header }} - {% endif %} -
- {% if column.localize == None %} - {{ cell }} - {% else %} - {% if column.localize %} - {{ cell|localize }} - {% else %} - {{ cell|unlocalize }} - {% endif %} - {% endif %}
{{ table.empty_text }}
{{ column.footer }}
- {% endblock table %} - - {% block pagination %} - {% if table.page and table.paginator.num_pages > 1 %} - - {% endif %} - {% endblock pagination %} -
+
+
+ {% include 'django_tables2/tailwind_inner.html' %} +
{% endblock table-wrapper %} diff --git a/src/templates/django_tables2/tailwind_inner.html b/src/templates/django_tables2/tailwind_inner.html new file mode 100644 index 00000000..24485f46 --- /dev/null +++ b/src/templates/django_tables2/tailwind_inner.html @@ -0,0 +1,120 @@ +{% load django_tables2 %} +{% load i18n %} + +{% block table %} + + {% block table.thead %} + {% if table.show_header %} + + + {% for column in table.columns %} + + {% endfor %} + + + {% endif %} + {% endblock table.thead %} + + + {% block table.tbody %} + + {% for row in table.paginated_rows %} + {% block table.tbody.row %} + + + {% for column, cell in row.items %} + + {% endfor %} + + {% endblock table.tbody.row %} + {% empty %} + {% if table.empty_text %} + {% block table.tbody.empty_text %} + + {% endblock table.tbody.empty_text %} + {% endif %} + {% endfor %} + + {% endblock table.tbody %} + + + {% block table.tfoot %} + {% if table.has_footer %} + + + {% for column in table.columns %} + + {% endfor %} + + + {% endif %} + {% endblock table.tfoot %} +
+ {% if column.orderable %} + {% comment%} + If the column is orderable, two small arrows will show next to the column name to signal that it can be sorted. + {% endcomment%} + + {{ column.header }} + + + {% else %} + {{ column.header }} + {% endif %} +
+ {% if column.localize == None %} + {{ cell }} + {% else %} + {% if column.localize %} + {{ cell|localize }} + {% else %} + {{ cell|unlocalize }} + {% endif %} + {% endif %}
{{ table.empty_text }}
{{ column.footer }}
+{% endblock table %} + +{% block pagination %} + {% if table.page and table.paginator.num_pages > 1 %} + + {% endif %} +{% endblock pagination %}