feat(tenancy): show merchant & store details on merchant user detail page

Add clickable card grids for owned merchants and store memberships,
replacing static count-only banners with linked summaries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-07 22:29:33 +01:00
parent d57f6a8ee6
commit cea8ac56f8
3 changed files with 110 additions and 22 deletions

View File

@@ -18,6 +18,8 @@ from app.modules.core.services.stats_aggregator import stats_aggregator
from app.modules.tenancy.services.admin_service import admin_service
from models.schema.auth import UserContext
from models.schema.auth import (
OwnedMerchantSummary,
StoreMembershipSummary,
UserCreate,
UserDeleteResponse,
UserDetailResponse,
@@ -76,6 +78,25 @@ def get_all_users(
is_email_verified=user.is_email_verified,
owned_merchants_count=len(user.owned_merchants) if user.owned_merchants else 0,
store_memberships_count=len(user.store_memberships) if user.store_memberships else 0,
owned_merchants=[
OwnedMerchantSummary(
id=m.id,
name=m.name,
is_active=m.is_active,
store_count=len(m.stores) if m.stores else 0,
)
for m in (user.owned_merchants or [])
],
store_memberships=[
StoreMembershipSummary(
store_id=su.store_id,
store_code=su.store.store_code,
store_name=su.store.name,
user_type=su.user_type,
is_active=su.is_active,
)
for su in (user.store_memberships or [])
],
)
for user in users
]
@@ -264,6 +285,25 @@ def get_user_details(
store_memberships_count=len(user.store_memberships)
if user.store_memberships
else 0,
owned_merchants=[
OwnedMerchantSummary(
id=m.id,
name=m.name,
is_active=m.is_active,
store_count=len(m.stores) if m.stores else 0,
)
for m in (user.owned_merchants or [])
],
store_memberships=[
StoreMembershipSummary(
store_id=su.store_id,
store_code=su.store.store_code,
store_name=su.store.name,
user_type=su.user_type,
is_active=su.is_active,
)
for su in (user.store_memberships or [])
],
)

View File

@@ -166,32 +166,59 @@
</div>
</div>
<!-- Merchant Ownership Info -->
<template x-if="merchantUser?.owned_merchants_count > 0">
<div class="px-4 py-3 mb-8 bg-orange-50 dark:bg-orange-900/20 rounded-lg border border-orange-200 dark:border-orange-800">
<div class="flex items-center">
<span x-html="$icon('office-building', 'w-5 h-5 text-orange-500 mr-3')"></span>
<div>
<h4 class="text-sm font-medium text-orange-800 dark:text-orange-300">Merchant Owner</h4>
<p class="text-sm text-orange-600 dark:text-orange-400">
This user owns <span x-text="merchantUser?.owned_merchants_count"></span> merchant(s) and has full access to their stores.
</p>
<!-- Owned Merchants -->
<template x-if="merchantUser?.owned_merchants?.length > 0">
<div class="mb-8">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
<span x-html="$icon('office-building', 'w-5 h-5 inline mr-2 text-orange-500')"></span>
Owned Merchants
</h3>
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<template x-for="merchant in merchantUser.owned_merchants" :key="merchant.id">
<a :href="'/admin/merchants/' + merchant.id"
class="block px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800 border border-transparent hover:border-orange-300 dark:hover:border-orange-600 transition-colors duration-150">
<div class="flex items-center justify-between mb-2">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-200 truncate" x-text="merchant.name"></h4>
<span class="inline-flex items-center px-2 py-0.5 text-xs font-semibold rounded-full"
:class="merchant.is_active ? 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100' : 'text-red-700 bg-red-100 dark:bg-red-700 dark:text-red-100'"
x-text="merchant.is_active ? 'Active' : 'Inactive'">
</span>
</div>
<div class="flex items-center text-xs text-gray-500 dark:text-gray-400">
<span x-html="$icon('store', 'w-3.5 h-3.5 mr-1')"></span>
<span x-text="merchant.store_count + ' store(s)'"></span>
</div>
</a>
</template>
</div>
</div>
</template>
<!-- Team Membership Info -->
<template x-if="merchantUser?.store_memberships_count > 0">
<div class="px-4 py-3 mb-8 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
<div class="flex items-center">
<span x-html="$icon('user-group', 'w-5 h-5 text-blue-500 mr-3')"></span>
<div>
<h4 class="text-sm font-medium text-blue-800 dark:text-blue-300">Store Team Member</h4>
<p class="text-sm text-blue-600 dark:text-blue-400">
This user is a team member on <span x-text="merchantUser?.store_memberships_count"></span> store(s).
</p>
<!-- Store Memberships -->
<template x-if="merchantUser?.store_memberships?.length > 0">
<div class="mb-8">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
<span x-html="$icon('user-group', 'w-5 h-5 inline mr-2 text-blue-500')"></span>
Store Memberships
</h3>
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<template x-for="membership in merchantUser.store_memberships" :key="membership.store_id">
<a :href="'/admin/stores/' + membership.store_code"
class="block px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800 border border-transparent hover:border-blue-300 dark:hover:border-blue-600 transition-colors duration-150">
<div class="flex items-center justify-between mb-2">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-200 truncate" x-text="membership.store_name"></h4>
<span class="inline-flex items-center px-2 py-0.5 text-xs font-semibold rounded-full"
:class="membership.is_active ? 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100' : 'text-red-700 bg-red-100 dark:bg-red-700 dark:text-red-100'"
x-text="membership.is_active ? 'Active' : 'Inactive'">
</span>
</div>
<div class="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
<span x-text="membership.store_code"></span>
<span class="inline-flex items-center px-2 py-0.5 rounded-full bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300"
x-text="membership.user_type"></span>
</div>
</a>
</template>
</div>
</div>
</template>

View File

@@ -49,6 +49,25 @@ class PlatformSelectResponse(BaseModel):
platform_code: str
class OwnedMerchantSummary(BaseModel):
"""Summary of a merchant owned by a user."""
id: int
name: str
is_active: bool
store_count: int
class StoreMembershipSummary(BaseModel):
"""Summary of a user's store membership."""
store_id: int
store_code: str
store_name: str
user_type: str
is_active: bool
class UserDetailResponse(UserResponse):
"""Extended user response with additional details."""
@@ -58,6 +77,8 @@ class UserDetailResponse(UserResponse):
is_email_verified: bool = False
owned_merchants_count: int = 0
store_memberships_count: int = 0
owned_merchants: list[OwnedMerchantSummary] = []
store_memberships: list[StoreMembershipSummary] = []
class UserUpdate(BaseModel):