Source code for ab.api.models.dashboard

"""Dashboard models for the ACPortal API."""

from __future__ import annotations

from datetime import datetime
from typing import Any, List, Optional
from uuid import UUID

from pydantic import Field

from ab.api.models.base import RequestModel, ResponseModel


[docs] class DashboardParams(RequestModel): """Query parameters for GET /dashboard.""" view_id: Optional[int] = Field(None, alias="viewId", description="Grid view ID") company_id: Optional[str] = Field(None, alias="companyId", description="Company UUID filter")
[docs] class DashboardCompanyParams(RequestModel): """Query parameters for GET /dashboard/gridviews (company filter).""" company_id: Optional[str] = Field(None, alias="companyId", description="Company UUID filter")
[docs] class DashboardCompanyRequest(RequestModel): """Body for POST /dashboard/{inbound,inhouse,outbound,...} endpoints.""" company_id: Optional[str] = Field(None, alias="companyId", description="Company UUID filter")
[docs] class DashboardItem(ResponseModel): """Row in the dashboard data grid — element of `DashboardSummary.data`.""" location: Optional[str] = Field(None, description="Location code") job_display_id: Optional[int] = Field(None, alias="jobDisplayID", description="Job display ID") pickup: Optional[str] = Field(None, description="Pickup company name") customer: Optional[str] = Field(None, description="Customer name") packer: Optional[str] = Field(None, description="Packer name") priority: Optional[int] = Field(None, description="Priority") next: Optional[str] = Field(None, description="Next step label") pause: Optional[int] = Field(None, description="Pause flag") labels: Optional[str] = Field(None, description="Labels") ops_form: Optional[str] = Field(None, alias="opsForm", description="Ops form") # A short label, not a number. Live values include "1 ", "2 ", "2.", "10" -- # padded, and "2." does not parse as an integer at all, so this was never an # int that happened to arrive as text. Kept as the string the API sends # rather than coerced, which would have to invent a reading for "2.". step: Optional[str] = Field(None, description="Workflow step label") ship_by: Optional[datetime] = Field(None, alias="shipBy", description="Ship-by date-time") expedite: Optional[str] = Field(None, description="Expedite flag") note: Optional[str] = Field(None, description="Note") carrier: Optional[str] = Field(None, description="Carrier name") labor: Optional[float] = Field(None, description="Estimated labor hours") actual: Optional[float] = Field(None, description="Actual labor hours") item_count: Optional[int] = Field(None, alias="item_Count", description="Item count") cust_ref: Optional[str] = Field(None, alias="custRef", description="Customer reference") requestor: Optional[str] = Field(None, description="Requesting company name") agent: Optional[str] = Field(None, description="Agent code") rep: Optional[str] = Field(None, description="Sales rep name") job_price: Optional[float] = Field(None, alias="jobPrice", description="Job price") basis: Optional[str] = Field(None, description="Pricing basis code") created: Optional[datetime] = Field(None, description="Job created date-time") full_age: Optional[int] = Field(None, alias="fullAge", description="Full age in days") held: Optional[int] = Field(None, description="Days held") net_age: Optional[int] = Field(None, alias="netAge", description="Net age in days") status: Optional[str] = Field(None, description="Job status label") next_status: Optional[str] = Field(None, alias="next_Status", description="Next status label") dims: Optional[str] = Field(None, description="Dimensions summary")
[docs] def cli_format(self) -> str: """One-line pretty row used by the CLI and examples (vs. JSON).""" ship = self.ship_by.strftime("%Y-%m-%d %H:%M") if self.ship_by else "—" job = "—" if self.job_display_id is None else str(self.job_display_id) step = "—" if self.step is None else str(self.step) return ( f"job={job:<8} " f"loc={(self.location or '—'):<6} " f"customer={(self.customer or '—')[:30]:<30} " f"step={step:<3} " f"next={(self.next or '—')[:14]:<14} " f"ship_by={ship:<16} " f"carrier={(self.carrier or '—')[:18]}" )
[docs] class DashboardSummary(ResponseModel): """Aggregated dashboard data — GET /dashboard.""" inbound_count: Optional[int] = Field(None, alias="inboundCount", description="Inbound shipment count") outbound_count: Optional[int] = Field(None, alias="outboundCount", description="Outbound shipment count") in_house_count: Optional[int] = Field(None, alias="inHouseCount", description="In-house shipment count") local_deliveries_count: Optional[int] = Field( None, alias="localDeliveriesCount", description="Local delivery count", ) recent_estimates_count: Optional[int] = Field( None, alias="recentEstimatesCount", description="Recent estimates count", ) data: Optional[List[DashboardItem]] = Field(None, description="Grid view rows")
[docs] def cli_format(self) -> str: """Pretty summary line + one line per row (vs. JSON). ``data`` is the active grid view's rows; counts are populated only when the API includes them. The header surfaces both so it's clear which fields were returned. """ rows = self.data or [] header = ( f"inbound={self.inbound_count} " f"outbound={self.outbound_count} " f"in_house={self.in_house_count} " f"local_deliveries={self.local_deliveries_count} " f"recent_estimates={self.recent_estimates_count} " f"rows={len(rows)}" ) if not rows: return header body = "\n".join(item.cli_format() for item in rows) return f"{header}\n{body}"
[docs] class GridViewState(ResponseModel): """Saved grid view state — GET/POST /dashboard/gridviewstate/{id}.""" id: Optional[str] = Field(None, description="View state ID") columns: Optional[List[dict]] = Field(None, description="Column configurations") filters: Optional[List[dict]] = Field(None, description="Active filters") sort_order: Optional[List[dict]] = Field(None, alias="sortOrder", description="Sort configuration")
[docs] class GridViewInfo(ResponseModel): """Grid view metadata — GET /dashboard/gridviews. Shape matches the swagger ``GridViewDetails`` component. ``id`` is the forward reference consumed by :class:`DashboardParams.view_id` when calling GET ``/dashboard``. """ id: Optional[int] = Field(None, description="View ID — feeds DashboardParams.view_id") name: Optional[str] = Field(None, description="View name") data_key: Optional[str] = Field(None, alias="dataKey", description="Data key") is_active: Optional[bool] = Field(None, alias="isActive", description="Whether the view is active") stored_procedure: Optional[str] = Field( None, alias="storedProcedure", description="Backing stored procedure", ) columns_specification: Optional[str] = Field( None, alias="columnsSpecification", description="Column specification (raw)", ) sp_columns: Optional[List[dict]] = Field( None, alias="spColumns", description="Stored procedure column metadata", )
[docs] def cli_format(self) -> str: """One-line pretty row used by the CLI and examples (vs. JSON).""" id_str = "—" if self.id is None else str(self.id) return f"id={id_str:<5} name={self.name!r:<30} dataKey={self.data_key!r:<20} active={self.is_active}"
[docs] class GridSorting(RequestModel): """SortingInfo body options for the 8_01 dashboard grids.""" selector: str | None = Field(None, alias="selector", description="Selector.") desc: bool = Field(False, alias="desc", description="Desc.")
[docs] class GridGrouping(RequestModel): """GroupingInfo body options for the 8_01 dashboard grids.""" selector: str | None = Field(None, alias="selector", description="Selector.") desc: bool = Field(False, alias="desc", description="Desc.") group_interval: str | None = Field(None, alias="groupInterval", description="Group interval.") is_expanded: bool | None = Field(None, alias="isExpanded", description="Is expanded.")
[docs] class GridSummary(RequestModel): """SummaryInfo body options for the 8_01 dashboard grids.""" selector: str | None = Field(None, alias="selector", description="Selector.") summary_type: str | None = Field(None, alias="summaryType", description="Summary type.")
[docs] class DashboardGridOptions(RequestModel): """WebApiDataSourceLoadOptions body options for the 8_01 dashboard grids.""" require_total_count: bool = Field(False, alias="requireTotalCount", description="Require total count.") require_group_count: bool = Field(False, alias="requireGroupCount", description="Require group count.") is_count_query: bool = Field(False, alias="isCountQuery", description="Is count query.") is_summary_query: bool = Field(False, alias="isSummaryQuery", description="Is summary query.") skip: int = Field(0, alias="skip", description="Skip.", ge=0) take: int = Field(0, alias="take", description="Take.", ge=0) sort: list[GridSorting] | None = Field(None, alias="sort", description="Sort.") group: list[GridGrouping] | None = Field(None, alias="group", description="Group.") filter: list[Any] | None = Field(None, alias="filter", description="Filter.") total_summary: list[GridSummary] | None = Field(None, alias="totalSummary", description="Total summary.") group_summary: list[GridSummary] | None = Field(None, alias="groupSummary", description="Group summary.") select: list[str] | None = Field(None, alias="select", description="Select.") pre_select: list[str] | None = Field(None, alias="preSelect", description="Pre select.") remote_select: bool | None = Field(None, alias="remoteSelect", description="Remote select.") remote_grouping: bool | None = Field(None, alias="remoteGrouping", description="Remote grouping.") expand_linq_sum_type: bool | None = Field(None, alias="expandLinqSumType", description="Expand linq sum type.") primary_key: list[str] | None = Field(None, alias="primaryKey", description="Primary key.") default_sort: str | None = Field(None, alias="defaultSort", description="Default sort.") string_to_lower: bool | None = Field(None, alias="stringToLower", description="String to lower.") paginate_via_primary_key: bool | None = Field( None, alias="paginateViaPrimaryKey", description="Paginate via primary key." ) sort_by_primary_key: bool | None = Field(None, alias="sortByPrimaryKey", description="Sort by primary key.") allow_async_over_sync: bool = Field(False, alias="allowAsyncOverSync", description="Allow async over sync.") search_value: str | None = Field(None, alias="searchValue", description="Search value.")
[docs] class DashboardGridParams(RequestModel): """Company scope is a query parameter, separate from grid load options.""" company_id: UUID = Field(alias="companyId", description="Company UUID whose jobs populate the grid.")