Select a package id to view its details and map below

Package detail

Head OfficePackage received29/06/25 16:45
Head OfficePackage transferred to carrier29/06/25 22:42
Glasgow International AirportEstimated arrival time at distribution centre01/07/25 17:13
Glasgow International AirportEstimated time package will be transferred to carrier02/07/25 00:30
Dortheys Hiyo Eluay International AirportEstimated arrival time at distribution centre02/07/25 17:27
Dortheys Hiyo Eluay International AirportEstimated time package will be transferred to carrier02/07/25 22:36
DestinationEstimated delivery time04/07/25 21:37
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)