feat(c7): dashboard widgets as plugin contributions + contact counts via contacts contract
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
This commit is contained in:
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from app.plugins.base import BasePlugin
|
||||
from app.plugins.manifest import (
|
||||
FrontendDashboardWidget,
|
||||
FrontendDetailTab,
|
||||
FrontendMenuItem,
|
||||
FrontendPageRoute,
|
||||
@@ -40,6 +41,18 @@ class CalendarPlugin(BasePlugin):
|
||||
],
|
||||
events=[],
|
||||
migrations=["0001_initial.sql", "0002_add_deleted_at.sql"],
|
||||
dashboard_widgets=[
|
||||
FrontendDashboardWidget(
|
||||
id="calendar_upcoming",
|
||||
label_key="dashboard.calendarUpcoming",
|
||||
label="Upcoming Appointments",
|
||||
component="@/components/dashboard/CalendarUpcomingWidget",
|
||||
icon="Calendar",
|
||||
order=30,
|
||||
col_span=1,
|
||||
permission="calendar:read",
|
||||
),
|
||||
],
|
||||
permissions=[
|
||||
"calendar:read",
|
||||
"calendar:write",
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Public contract for the contacts plugin.
|
||||
|
||||
Exposes the symbols that other core modules and plugins need without
|
||||
importing from internal modules directly (Block C7: dashboard counts).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.visibility import apply_visibility_filter
|
||||
from app.models.contact import Contact
|
||||
from app.plugins.builtins.contracts import get_contract_registry
|
||||
|
||||
|
||||
class ContactsContract:
|
||||
"""Public API surface for the contacts plugin."""
|
||||
|
||||
contract_name = "contacts"
|
||||
|
||||
@staticmethod
|
||||
async def get_counts(
|
||||
db: AsyncSession,
|
||||
tenant_id: Any,
|
||||
user_id: Any,
|
||||
is_system_admin: bool = False,
|
||||
) -> dict[str, int]:
|
||||
"""Return visibility-filtered contact/company/person counts."""
|
||||
queries = []
|
||||
for type_filter in (None, "company", "person"):
|
||||
query = select(func.count(Contact.id)).where(
|
||||
Contact.tenant_id == tenant_id,
|
||||
Contact.deleted_at.is_(None),
|
||||
)
|
||||
if type_filter is not None:
|
||||
query = query.where(Contact.type == type_filter)
|
||||
query = await apply_visibility_filter(
|
||||
db, query, "contact", Contact, user_id, tenant_id, is_system_admin
|
||||
)
|
||||
queries.append(query)
|
||||
|
||||
results = [((await db.execute(q)).scalar() or 0) for q in queries]
|
||||
return {
|
||||
"contacts": results[0],
|
||||
"companies": results[1],
|
||||
"persons": results[2],
|
||||
"total": results[0],
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_function(cls, name: str):
|
||||
"""Return a callable exposed by this contract, or None if absent."""
|
||||
return getattr(cls, name, None)
|
||||
|
||||
|
||||
# ─── self-registration ───
|
||||
|
||||
_contract = ContactsContract()
|
||||
get_contract_registry().register("contacts", _contract)
|
||||
@@ -9,7 +9,11 @@ from __future__ import annotations
|
||||
import logging
|
||||
|
||||
from app.plugins.base import BasePlugin
|
||||
from app.plugins.manifest import PluginManifest, PluginRouteDef
|
||||
from app.plugins.manifest import (
|
||||
FrontendDashboardWidget,
|
||||
PluginManifest,
|
||||
PluginRouteDef,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -54,6 +58,18 @@ class ContactsPlugin(BasePlugin):
|
||||
],
|
||||
events=[],
|
||||
migrations=[],
|
||||
dashboard_widgets=[
|
||||
FrontendDashboardWidget(
|
||||
id="recent_contacts",
|
||||
label_key="dashboard.recentContacts",
|
||||
label="Recent Contacts",
|
||||
component="@/components/dashboard/RecentContactsWidget",
|
||||
icon="Users",
|
||||
order=10,
|
||||
col_span=2,
|
||||
permission="contacts:read",
|
||||
),
|
||||
],
|
||||
permissions=[
|
||||
"contacts:read",
|
||||
"contacts:write",
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from app.plugins.base import BasePlugin
|
||||
from app.plugins.manifest import (
|
||||
CronJobContribution,
|
||||
FrontendDashboardWidget,
|
||||
FrontendMenuItem,
|
||||
FrontendPageRoute,
|
||||
PluginManifest,
|
||||
@@ -30,6 +31,18 @@ class TasksPlugin(BasePlugin):
|
||||
],
|
||||
events=[],
|
||||
migrations=["0001_initial.sql", "0002_unified_task_system.sql"],
|
||||
dashboard_widgets=[
|
||||
FrontendDashboardWidget(
|
||||
id="tasks_summary",
|
||||
label_key="dashboard.tasksSummary",
|
||||
label="Tasks Summary",
|
||||
component="@/components/dashboard/TasksSummaryWidget",
|
||||
icon="CheckSquare",
|
||||
order=20,
|
||||
col_span=1,
|
||||
permission="tasks:read",
|
||||
),
|
||||
],
|
||||
permissions=[
|
||||
"tasks:read",
|
||||
"tasks:write",
|
||||
|
||||
+11
-48
@@ -5,13 +5,11 @@ from __future__ import annotations
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.db import get_db
|
||||
from app.core.visibility import apply_visibility_filter
|
||||
from app.deps import require_permission
|
||||
from app.models.contact import Contact
|
||||
from app.plugins.builtins.contracts import get_contract
|
||||
from app.plugins.registry import get_registry
|
||||
|
||||
router = APIRouter(prefix="/api/v1/dashboard", tags=["dashboard"])
|
||||
@@ -50,51 +48,16 @@ async def get_dashboard_counts(
|
||||
):
|
||||
"""Get dashboard count statistics filtered by user visibility.
|
||||
|
||||
Returns counts for contacts and companies that the current user
|
||||
is allowed to see based on ownership and sharing permissions.
|
||||
Counts come from the contacts plugin contract (Block C7) — the core
|
||||
dashboard route has no direct dependency on the Contact model.
|
||||
"""
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
user_id = uuid.UUID(current_user["user_id"])
|
||||
is_system_admin = current_user.get("is_system_admin", False)
|
||||
contacts_contract = get_contract("contacts")
|
||||
if contacts_contract is None or not hasattr(contacts_contract, "get_counts"):
|
||||
return {"contacts": 0, "companies": 0, "persons": 0, "total": 0}
|
||||
|
||||
# Contact count with visibility filter
|
||||
contact_query = select(func.count(Contact.id)).where(
|
||||
Contact.tenant_id == tenant_id,
|
||||
Contact.deleted_at.is_(None),
|
||||
return await contacts_contract.get_counts(
|
||||
db=db,
|
||||
tenant_id=uuid.UUID(current_user["tenant_id"]),
|
||||
user_id=uuid.UUID(current_user["user_id"]),
|
||||
is_system_admin=current_user.get("is_system_admin", False),
|
||||
)
|
||||
contact_query = await apply_visibility_filter(
|
||||
db, contact_query, "contact", Contact, user_id, tenant_id, is_system_admin
|
||||
)
|
||||
contact_result = await db.execute(contact_query)
|
||||
contact_count = contact_result.scalar() or 0
|
||||
|
||||
# Company count (Contact.type == 'company') with visibility filter
|
||||
company_query = select(func.count(Contact.id)).where(
|
||||
Contact.tenant_id == tenant_id,
|
||||
Contact.deleted_at.is_(None),
|
||||
Contact.type == "company",
|
||||
)
|
||||
company_query = await apply_visibility_filter(
|
||||
db, company_query, "contact", Contact, user_id, tenant_id, is_system_admin
|
||||
)
|
||||
company_result = await db.execute(company_query)
|
||||
company_count = company_result.scalar() or 0
|
||||
|
||||
# Person count (Contact.type == 'person') with visibility filter
|
||||
person_query = select(func.count(Contact.id)).where(
|
||||
Contact.tenant_id == tenant_id,
|
||||
Contact.deleted_at.is_(None),
|
||||
Contact.type == "person",
|
||||
)
|
||||
person_query = await apply_visibility_filter(
|
||||
db, person_query, "contact", Contact, user_id, tenant_id, is_system_admin
|
||||
)
|
||||
person_result = await db.execute(person_query)
|
||||
person_count = person_result.scalar() or 0
|
||||
|
||||
return {
|
||||
"contacts": contact_count,
|
||||
"companies": company_count,
|
||||
"persons": person_count,
|
||||
"total": contact_count,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user