diff --git a/README.md b/README.md index 60a4628..07100ad 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,20 @@ # transcribe-ui User interface for the SUNET transcription service +## Features + +### Admin Dashboard +The admin dashboard provides comprehensive management capabilities including: +- **Group Management**: Create and manage user groups with transcription quotas +- **User Management**: Enable/disable users and assign admin roles +- **Customer Management**: View and manage customer accounts +- **Price Plan Display**: View current price plan and remaining blocks (for fixed plans) + - Shows plan name and type + - Displays blocks remaining with visual progress indicator + - Color-coded warnings when blocks are running low +- **Statistics**: View detailed transcription statistics per group and user +- **Health Monitoring**: Monitor system health and worker status (BOFH users only) + ## Development environment 1. Edit the environment settings, should be in a file named `.env`. The following settings should be sufficient for most cases: diff --git a/pages/admin.py b/pages/admin.py index b4646f6..7cb273e 100644 --- a/pages/admin.py +++ b/pages/admin.py @@ -44,6 +44,22 @@ def user_statistics_get(group_id: str) -> dict: print(f"Error fetching user statistics: {e}") return {} +def priceplan_get() -> dict: + """ + Fetch price plan information from backend. + """ + + try: + res = requests.get( + settings.API_URL + "/api/v1/admin/priceplan", headers=get_auth_header() + ) + res.raise_for_status() + + return res.json() + except requests.RequestException as e: + print(f"Error fetching price plan: {e}") + return {} + def create_group_dialog(page: callable) -> None: with ui.dialog() as create_group_dialog: with ui.card().style("width: 500px; max-width: 90vw;"): @@ -581,6 +597,58 @@ def statistics(group_id: str) -> None: ui.icon("search") +def create_priceplan_card() -> None: + """ + Create a card to display price plan information for admins. + """ + priceplan_data = priceplan_get() + + if not priceplan_data: + return + + try: + result = priceplan_data.get("result", {}) + plan_type = result.get("plan_type", "Unknown") + plan_name = result.get("plan_name", "Unknown") + blocks_remaining = result.get("blocks_remaining") + total_blocks = result.get("total_blocks") + + with ui.card().classes("my-2").style("width: 100%; box-shadow: none; border: 2px solid #082954; padding: 16px; background-color: #f8f9fa;"): + with ui.row().style("justify-content: space-between; align-items: center; width: 100%;"): + with ui.column().style("flex: 1;"): + ui.label("Current Price Plan").classes("text-h5 font-bold").style("color: #082954;") + ui.label(f"Plan: {plan_name}").classes("text-lg font-medium") + ui.label(f"Type: {plan_type}").classes("text-md") + + # Display blocks remaining if it's a fixed plan + if plan_type.lower() == "fixed" and blocks_remaining is not None: + with ui.row().classes("items-center gap-2 mt-2"): + ui.icon("inventory_2").classes("text-2xl").style("color: #082954;") + if total_blocks is not None: + ui.label(f"Blocks remaining: {blocks_remaining} / {total_blocks}").classes("text-lg font-semibold").style("color: #082954;") + else: + ui.label(f"Blocks remaining: {blocks_remaining}").classes("text-lg font-semibold").style("color: #082954;") + + # Add a progress bar for visual representation + if total_blocks is not None and total_blocks > 0: + percentage = (blocks_remaining / total_blocks) * 100 + with ui.column().style("width: 100%; margin-top: 8px;"): + ui.linear_progress(value=percentage/100).props(f"color={'positive' if percentage > 50 else 'warning' if percentage > 20 else 'negative'}") + + # Show warning if blocks are running low + if percentage <= 20 and blocks_remaining > 0: + with ui.row().classes("items-center gap-2 mt-2"): + ui.icon("warning").classes("text-xl").style("color: #f57c00;") + ui.label("Warning: Blocks running low!").classes("text-sm font-medium").style("color: #f57c00;") + elif blocks_remaining == 0: + with ui.row().classes("items-center gap-2 mt-2"): + ui.icon("error").classes("text-xl").style("color: #d32f2f;") + ui.label("No blocks remaining!").classes("text-sm font-medium").style("color: #d32f2f;") + + except (KeyError, TypeError) as e: + print(f"Error displaying price plan: {e}") + + def create() -> None: @ui.refreshable @ui.page("/admin") @@ -630,28 +698,33 @@ def admin() -> None: .props("color=white flat") ) customers.on("click", lambda: ui.navigate.to("/admin/customers")) - groups = groups_get() - if not groups: - ui.label("No groups found. Create a new group to get started.").classes("text-lg") - return - with ui.scroll_area().style("height: calc(100vh - 160px); width: 100%;"): - groups = sorted( - groups_get()["result"], - key=lambda x: (x["name"].lower() != "all users", x["name"].lower()) + # Display price plan information + create_priceplan_card() + + groups = groups_get() + + if not groups: + ui.label("No groups found. Create a new group to get started.").classes("text-lg") + return + + with ui.scroll_area().style("height: calc(100vh - 160px); width: 100%;"): + groups = sorted( + groups_get()["result"], + key=lambda x: (x["name"].lower() != "all users", x["name"].lower()) + ) + for group in groups: + g = Group( + group_id=group["id"], + name=group["name"], + description=group["description"], + created_at=group["created_at"], + users=group["users"], + nr_users=group["nr_users"], + stats=group["stats"], + quota_seconds=group["quota_seconds"], ) - for group in groups: - g = Group( - group_id=group["id"], - name=group["name"], - description=group["description"], - created_at=group["created_at"], - users=group["users"], - nr_users=group["nr_users"], - stats=group["stats"], - quota_seconds=group["quota_seconds"], - ) - g.create_card() + g.create_card() @ui.page("/admin/users") def users() -> None: