diff --git a/app/main/views/organizations.py b/app/main/views/organizations.py index 52acc144d..51c8cb3fb 100644 --- a/app/main/views/organizations.py +++ b/app/main/views/organizations.py @@ -76,6 +76,39 @@ def get_organization_message_allowance(org_id): } +def get_services_dashboard_data(organization, year): + try: + dashboard_data = organizations_client.get_organization_dashboard(organization.id, year) + services = dashboard_data.get("services", []) + except Exception as e: + current_app.logger.error(f"Error fetching dashboard data: {e}") + return [] + + for service in services: + service["id"] = service.get("service_id") + service["name"] = service.get("service_name") + service["recent_template"] = service.get("recent_sms_template_name") or "N/A" + service["primary_contact"] = service.get("primary_contact") or "N/A" + + emails_sent = service.get("emails_sent", 0) + sms_sent = service.get("sms_billable_units", 0) + sms_remainder = service.get("sms_remainder", 0) + sms_cost = service.get("sms_cost", 0) + + usage_parts = [] + if emails_sent > 0: + usage_parts.append(f"{emails_sent:,} emails") + if sms_sent > 0 or sms_remainder > 0: + if sms_cost > 0: + usage_parts.append(f"{sms_sent:,} sms ({sms_remainder:,} remaining, ${sms_cost:,.2f})") + else: + usage_parts.append(f"{sms_sent:,} sms ({sms_remainder:,} remaining)") + + service["usage"] = ", ".join(usage_parts) if usage_parts else "No usage" + + return services + + @main.route("/organizations/", methods=["GET"]) @user_has_permissions() def organization_dashboard(org_id): @@ -86,9 +119,12 @@ def organization_dashboard(org_id): message_allowance = get_organization_message_allowance(org_id) + services_with_usage = get_services_dashboard_data(current_organization, year) + return render_template( "views/organizations/organization/index.html", selected_year=year, + services=services_with_usage, live_services=len(current_organization.live_services), trial_services=len(current_organization.trial_services), suspended_services=len(current_organization.suspended_services), diff --git a/app/notify_client/organizations_api_client.py b/app/notify_client/organizations_api_client.py index e7cca58ef..e68b23f35 100644 --- a/app/notify_client/organizations_api_client.py +++ b/app/notify_client/organizations_api_client.py @@ -83,5 +83,11 @@ class OrganizationsClient(NotifyAdminAPIClient): url="/organizations/{}/message-allowance".format(org_id), ) + def get_organization_dashboard(self, org_id, year): + return self.get( + url=f"/organizations/{org_id}/dashboard", + params={"year": str(year)}, + ) + organizations_client = OrganizationsClient() diff --git a/app/templates/views/organizations/organization/index.html b/app/templates/views/organizations/organization/index.html index 88a2d2433..6afc33c4d 100644 --- a/app/templates/views/organizations/organization/index.html +++ b/app/templates/views/organizations/organization/index.html @@ -58,19 +58,65 @@ +
+ What is a service? +
+

+ When you join Notify, you're added to a service. This is your organization's workspace for sending text messages and emails. Within your service, you can: +

+
    +
  1. Create and edit message templates
  2. +
  3. Send messages to recipients
  4. +
  5. View message status and history
  6. +
  7. Manage team members and permissions
  8. +
  9. Track usage and delivery statistics
  10. +
  11. If you work for multiple organizations, you may belong to multiple services and can switch between them.
  12. +
+
+
+ + +
-

What is a service?

-

- When you join Notify, you're added to a service. This is your organization's workspace for sending text messages and emails. Within your service, you can: -

-
    -
  1. Create and edit message templates
  2. -
  3. Send messages to recipients
  4. -
  5. View message status and history
  6. -
  7. Manage team members and permissions
  8. -
  9. Track usage and delivery statistics
  10. -
  11. If you work for multiple organizations, you may belong to multiple services and can switch between them.
  12. -
+

Services Overview

+
+ + + + + + + + + + + + {% if services %} + {% for service in services %} + + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
NameStatusUsagePrimary ContactRecent Template Used
{{ service.name }} + {% if not service.active %} + Suspended + {% elif service.restricted %} + Trial + {% else %} + Live + {% endif %} + {{ service.usage }}{{ service.primary_contact }}{{ service.recent_template }}
No services found within this organization
+
{% endblock %} diff --git a/tests/app/main/views/organizations/test_organizations.py b/tests/app/main/views/organizations/test_organizations.py index 3dc7e74d8..70389b8d6 100644 --- a/tests/app/main/views/organizations/test_organizations.py +++ b/tests/app/main/views/organizations/test_organizations.py @@ -1538,6 +1538,11 @@ def test_organization_dashboard_shows_message_usage( "app.organizations_client.get_organization_services", return_value=[], ) + mocker.patch( + "app.organizations_client.get_organization_dashboard", + return_value={"services": []}, + ) + client_request.login(active_user_with_permissions) page = client_request.get( ".organization_dashboard", @@ -1576,6 +1581,16 @@ def test_organization_dashboard_shows_service_counts( service_json(id_="3", name="Suspended", restricted=False, active=False), ], ) + mocker.patch( + "app.organizations_client.get_organization_dashboard", + return_value={ + "services": [ + {"service_id": "1", "service_name": "Live Service", "active": True, "restricted": False}, + {"service_id": "2", "service_name": "Trial Service", "active": True, "restricted": True}, + {"service_id": "3", "service_name": "Suspended", "active": False, "restricted": False}, + ] + }, + ) client_request.login(active_user_with_permissions) page = client_request.get( @@ -1592,3 +1607,85 @@ def test_organization_dashboard_shows_service_counts( assert "1 Live" in normalize_spaces(service_box.text) assert "1 Trial" in normalize_spaces(service_box.text) assert "1 Suspended" in normalize_spaces(service_box.text) + + +def test_organization_dashboard_services_table( + client_request, + mock_get_organization, + mocker, + active_user_with_permissions, +): + mocker.patch.dict("flask.current_app.config", {"ORGANIZATION_DASHBOARD_ENABLED": True}) + + mocker.patch( + "app.organizations_client.get_organization_message_usage", + return_value={"messages_sent": 0, "messages_remaining": 0, "total_message_limit": 0}, + ) + mocker.patch( + "app.organizations_client.get_organization_services", + return_value=[ + service_json(id_="1", name="Service One", restricted=False, active=True), + service_json(id_="2", name="Service Two", restricted=True, active=True), + ], + ) + mocker.patch( + "app.organizations_client.get_organization_dashboard", + return_value={ + "services": [ + { + "service_id": "1", + "service_name": "Service One", + "active": True, + "restricted": False, + "emails_sent": 1500, + "sms_billable_units": 500, + "sms_remainder": 249500, + "sms_cost": 42.75, + "recent_sms_template_name": "Welcome SMS", + "primary_contact": None, + }, + { + "service_id": "2", + "service_name": "Service Two", + "active": True, + "restricted": True, + "emails_sent": 250, + "sms_billable_units": 100, + "sms_remainder": 249900, + "sms_cost": 0, + "recent_sms_template_name": "Reminder SMS", + "primary_contact": None, + }, + ] + }, + ) + + client_request.login(active_user_with_permissions) + page = client_request.get( + ".organization_dashboard", + org_id=ORGANISATION_ID, + ) + + table = page.select_one("table.usa-table") + assert table is not None + + rows = table.select("tbody tr") + assert len(rows) == 2 + + first_row_cells = rows[0].select("td") + assert normalize_spaces(first_row_cells[0].text) == "Service One" + assert normalize_spaces(first_row_cells[1].text) == "Live" + assert "1,500 emails" in normalize_spaces(first_row_cells[2].text) + assert "500 sms" in normalize_spaces(first_row_cells[2].text) + assert "249,500 remaining" in normalize_spaces(first_row_cells[2].text) + assert normalize_spaces(first_row_cells[3].text) == "N/A" + assert normalize_spaces(first_row_cells[4].text) == "Welcome SMS" + + second_row_cells = rows[1].select("td") + assert normalize_spaces(second_row_cells[0].text) == "Service Two" + assert normalize_spaces(second_row_cells[1].text) == "Trial" + assert "250 emails" in normalize_spaces(second_row_cells[2].text) + assert "100 sms" in normalize_spaces(second_row_cells[2].text) + assert "249,900 remaining" in normalize_spaces(second_row_cells[2].text) + assert normalize_spaces(second_row_cells[3].text) == "N/A" + assert normalize_spaces(second_row_cells[4].text) == "Reminder SMS"