Select a package id to view its details and map below

Package detail

Head OfficePackage received17/05/24 20:56
Head OfficePackage transferred to carrier18/05/24 02:53
Glasgow International AirportEstimated arrival time at distribution centre19/05/24 21:24
Glasgow International AirportEstimated time package will be transferred to carrier20/05/24 04:41
Dortheys Hiyo Eluay International AirportEstimated arrival time at distribution centre20/05/24 21:38
Dortheys Hiyo Eluay International AirportEstimated time package will be transferred to carrier21/05/24 02:47
DestinationEstimated delivery time23/05/24 01:48
from django import forms
from django.db.models import Min
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _

import pandas as pd
import plotly.graph_objects as go
from dashboards.component import Form, Map, Text
from dashboards.component.chart import ChartSerializer
from dashboards.component.layout import Card, ComponentLayout
from dashboards.dashboard import Dashboard
from dashboards.forms import DashboardForm
from dashboards.registry import registry

from example.packages.models import Package, PackageStage


def get_package(filters):
    _id = (filters or {}).get("package_id")
    qs = Package.objects
    if _id:
        qs = qs.filter(id=_id)

    return qs.first()


class PackageForm(DashboardForm):
    package_id = forms.ModelChoiceField(
        queryset=Package.objects.all(),
        help_text=_("Select a package id to view its details and map below"),
        empty_label=None,
    )


def get_detail_table(*args, filters=None, **kwargs):
    package = get_package(filters)
    if not package:
        content = "<p>Select a package to see it's details.</p>"
    else:
        stages = (
            PackageStage.objects.filter(location__package=package)
            .exclude(description="Current position")
            .order_by("time_offset")
        )

        _now = now()
        rows = map(
            lambda s: (
                "<tr>"
                + f"<td style='text-align: left; padding-right: 1em'>{s.location.label}</td>"
                + f"<td style='text-align: left; padding-right: 1em'>{s.description}</td>"
                + f"<td style='white-space: nowrap'>{(_now + s.time_offset).strftime('%d/%m/%y %H:%M')}</td>"
                + "</tr>"
            ),
            stages,
        )

        content = f"<table class='table'><tbody>{''.join(rows)}</tbody></table>"

    return f"<h2>Package detail</h2>{content}"


class PackageMapSerializer(ChartSerializer):
    def get_data(self, *args, filters=None, **kwargs) -> pd.DataFrame:
        package = get_package(filters)
        if not package:
            return None

        _now = now()

        locations = package.locations.annotate(
            first_stage_date=Min("stages__time_offset")
        ).order_by("first_stage_date")

        return pd.DataFrame(
            [
                {
                    "lat": location.location.y,
                    "lon": location.location.x,
                    "text": f"{location.label}<br><br>"
                    + "<br>".join(
                        f"{stage.description}: {(_now + stage.time_offset).strftime('%d/%m/%y %H:%M')}"
                        for stage in location.stages.order_by("time_offset")
                    ),
                }
                for location in locations
            ]
        )

    def to_fig(self, data):
        fig = go.Figure()

        if data is None:
            fig.add_trace(go.Scattergeo())
        else:
            fig.add_trace(
                go.Scattergeo(
                    lon=data["lon"],
                    lat=data["lat"],
                    hoverinfo="text",
                    text=data["text"],
                    mode="lines+markers",
                )
            )
            fig.update_layout(
                geo={
                    "lonaxis": {
                        "range": [min(data["lon"]) - 10, max(data["lon"]) + 10],
                    },
                    "lataxis": {
                        "range": [min(data["lat"]) - 10, max(data["lat"]) + 10],
                    },
                }
            )

        return fig


class PackagesMapDashboard(Dashboard):
    package_form = Form(form=PackageForm, dependents=["package_map", "package_details"])
    package_details = Text(value=get_detail_table, mark_safe=True)
    package_map = Map(value=PackageMapSerializer)

    class Meta:
        name = "Package Map"

    class Layout(Dashboard.Layout):
        components = ComponentLayout(
            Card("package_form", grid_css_classes="span-12"),
            Card("package_details", grid_css_classes="span-6"),
            Card("package_map", grid_css_classes="span-6"),
        )


registry.register(PackagesMapDashboard)