from __future__ import annotations from .crm_mixin import CrmMixin from .crm_schema_helpers import _field_path from .crm_shared import ( CUSTOMER_STATUS_LABELS, FOLLOWUP_STATUS_LABELS, FOLLOWUP_TYPE_LABELS, MAX_PAGE_SIZE, MAX_SCAN_RECORDS, Any, Mapping, NbCommandError, _decorate_dimension_counts, _dimension_value, _first_numeric, build_and_filter, build_date_range_condition, build_relation_id_condition, ) class CrmAnalyticsMixin(CrmMixin): def crm_overview( self, date_from: str | None = None, date_to: str | None = None, owner: str | None = None, ) -> dict[str, Any]: owner_id, owner_error = self._resolve_owner_id_or_error(owner) if owner_error: return {"error": owner_error} customer_filter = build_and_filter([build_relation_id_condition(self.config.customer.owner_field, owner_id)]) followup_filter = build_and_filter( [ build_relation_id_condition(self.config.followup.owner_field, owner_id), build_date_range_condition(self.config.followup.date_field, date_from, date_to), ] ) totals = { "customers": self._count(self.config.customer.resource, self.config.customer.id_field, customer_filter), "contacts": self._count(self.config.contact.resource, self.config.contact.id_field, {}), "followups": self._count(self.config.followup.resource, self.config.followup.id_field, followup_filter), } overview: dict[str, Any] = { "totals": totals, "customer_status": self._group_count( self.config.customer.resource, self.config.customer.id_field, self.config.customer.status_field, customer_filter, ), "customer_source": self._group_count( self.config.customer.resource, self.config.customer.id_field, self.config.customer.source_field, customer_filter, ), "customer_owner": self._group_count( self.config.customer.resource, self.config.customer.id_field, [self.config.customer.owner_field, "id"], customer_filter, ), "recent_followups": self._compact_followups( self._data( self.client.list_records( self.config.followup.resource, filter=followup_filter, fields=self._followup_compact_fields(), sort=self.config.followup.sort, page=1, page_size=10, ) ) ), } overview["customer_status"] = self._decorate_status_counts(overview["customer_status"]) overview["customer_source"] = self._decorate_source_counts(overview["customer_source"]) overview["customer_owner"] = self._decorate_owner_counts(overview["customer_owner"]) return overview def crm_filter_options(self, owner_query: str | None = None) -> dict[str, Any]: owner_candidates: list[dict[str, Any]] = [] if (owner_query or "").strip(): owner_candidates = self.resolve_owner(owner_query or "")["results"] return { "customer_status": self._decorate_status_counts( self._group_count( self.config.customer.resource, self.config.customer.id_field, self.config.customer.status_field, {}, ) ), "customer_source": self._decorate_source_counts( self._group_count( self.config.customer.resource, self.config.customer.id_field, self.config.customer.source_field, {}, ) ), # Follow-up rows render type and status as labels only, so the raw # codes the filters expect are published here instead. "followup_type": [{"value": value, "label": label} for value, label in FOLLOWUP_TYPE_LABELS.items()], "followup_record_status": [ {"value": value, "label": label} for value, label in FOLLOWUP_STATUS_LABELS.items() ], "owner_candidates": owner_candidates, "max_limit": MAX_PAGE_SIZE, } def _count(self, resource: str, id_field: str, filter_value: Mapping[str, Any]) -> int: response = self.client.query_records( resource, measures=[{"field": [id_field], "aggregation": "count", "alias": "count"}], filter=filter_value, limit=1, ) return _first_numeric(self._data(response), "count") def _group_count( self, resource: str, id_field: str, dimension_field: str | list[str], filter_value: Mapping[str, Any], ) -> list[dict[str, Any]]: if not dimension_field: return [] dimension_path = _field_path(dimension_field) dimension_alias = "_".join(dimension_path) rows: list[dict[str, Any]] = [] batch_size = MAX_PAGE_SIZE offset = 0 while True: response = self.client.query_records( resource, dimensions=[{"field": dimension_path, "alias": dimension_alias}], measures=[{"field": [id_field], "aggregation": "count", "alias": "count"}], orders=[{"field": dimension_path, "alias": dimension_alias, "order": "asc"}], filter=filter_value, limit=batch_size, offset=offset, ) batch = self._data(response) rows.extend(batch) if len(batch) < batch_size: break if len(rows) >= MAX_SCAN_RECORDS: raise NbCommandError( f"Aggregation produced at least {MAX_SCAN_RECORDS} buckets; add filters to narrow the result" ) offset += batch_size return rows def _decorate_status_counts(self, rows: list[dict[str, Any]]) -> list[dict[str, Any]]: return _decorate_dimension_counts( rows, preferred_keys=[self.config.customer.status_field, "kh_progress", "status"], labels=CUSTOMER_STATUS_LABELS, ) def _decorate_source_counts(self, rows: list[dict[str, Any]]) -> list[dict[str, Any]]: return _decorate_dimension_counts( rows, preferred_keys=[self.config.customer.source_field, "f_225h6n6v654", "source"], labels={}, ) def _decorate_owner_counts(self, rows: list[dict[str, Any]]) -> list[dict[str, Any]]: owner_alias = "_".join(_field_path([self.config.customer.owner_field, "id"])) decorated: list[dict[str, Any]] = [] for row in rows: value, key = _dimension_value(row, [owner_alias, "owner_id", "kehu_followup_person_id"]) item = dict(row) if value is not None: if key is not None and key not in {"owner_id", "value"}: item.pop(key, None) item["owner_id"] = value decorated.append(item) return decorated