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:
@@ -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 app.modules.tenancy.services.admin_service import admin_service
|
||||||
from models.schema.auth import UserContext
|
from models.schema.auth import UserContext
|
||||||
from models.schema.auth import (
|
from models.schema.auth import (
|
||||||
|
OwnedMerchantSummary,
|
||||||
|
StoreMembershipSummary,
|
||||||
UserCreate,
|
UserCreate,
|
||||||
UserDeleteResponse,
|
UserDeleteResponse,
|
||||||
UserDetailResponse,
|
UserDetailResponse,
|
||||||
@@ -76,6 +78,25 @@ def get_all_users(
|
|||||||
is_email_verified=user.is_email_verified,
|
is_email_verified=user.is_email_verified,
|
||||||
owned_merchants_count=len(user.owned_merchants) if user.owned_merchants else 0,
|
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,
|
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
|
for user in users
|
||||||
]
|
]
|
||||||
@@ -264,6 +285,25 @@ def get_user_details(
|
|||||||
store_memberships_count=len(user.store_memberships)
|
store_memberships_count=len(user.store_memberships)
|
||||||
if user.store_memberships
|
if user.store_memberships
|
||||||
else 0,
|
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 [])
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -166,32 +166,59 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Merchant Ownership Info -->
|
<!-- Owned Merchants -->
|
||||||
<template x-if="merchantUser?.owned_merchants_count > 0">
|
<template x-if="merchantUser?.owned_merchants?.length > 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="mb-8">
|
||||||
<div class="flex items-center">
|
<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 text-orange-500 mr-3')"></span>
|
<span x-html="$icon('office-building', 'w-5 h-5 inline mr-2 text-orange-500')"></span>
|
||||||
<div>
|
Owned Merchants
|
||||||
<h4 class="text-sm font-medium text-orange-800 dark:text-orange-300">Merchant Owner</h4>
|
</h3>
|
||||||
<p class="text-sm text-orange-600 dark:text-orange-400">
|
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
This user owns <span x-text="merchantUser?.owned_merchants_count"></span> merchant(s) and has full access to their stores.
|
<template x-for="merchant in merchantUser.owned_merchants" :key="merchant.id">
|
||||||
</p>
|
<a :href="'/admin/merchants/' + merchant.id"
|
||||||
</div>
|
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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Team Membership Info -->
|
<!-- Store Memberships -->
|
||||||
<template x-if="merchantUser?.store_memberships_count > 0">
|
<template x-if="merchantUser?.store_memberships?.length > 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="mb-8">
|
||||||
<div class="flex items-center">
|
<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 text-blue-500 mr-3')"></span>
|
<span x-html="$icon('user-group', 'w-5 h-5 inline mr-2 text-blue-500')"></span>
|
||||||
<div>
|
Store Memberships
|
||||||
<h4 class="text-sm font-medium text-blue-800 dark:text-blue-300">Store Team Member</h4>
|
</h3>
|
||||||
<p class="text-sm text-blue-600 dark:text-blue-400">
|
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
This user is a team member on <span x-text="merchantUser?.store_memberships_count"></span> store(s).
|
<template x-for="membership in merchantUser.store_memberships" :key="membership.store_id">
|
||||||
</p>
|
<a :href="'/admin/stores/' + membership.store_code"
|
||||||
</div>
|
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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -49,6 +49,25 @@ class PlatformSelectResponse(BaseModel):
|
|||||||
platform_code: str
|
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):
|
class UserDetailResponse(UserResponse):
|
||||||
"""Extended user response with additional details."""
|
"""Extended user response with additional details."""
|
||||||
|
|
||||||
@@ -58,6 +77,8 @@ class UserDetailResponse(UserResponse):
|
|||||||
is_email_verified: bool = False
|
is_email_verified: bool = False
|
||||||
owned_merchants_count: int = 0
|
owned_merchants_count: int = 0
|
||||||
store_memberships_count: int = 0
|
store_memberships_count: int = 0
|
||||||
|
owned_merchants: list[OwnedMerchantSummary] = []
|
||||||
|
store_memberships: list[StoreMembershipSummary] = []
|
||||||
|
|
||||||
|
|
||||||
class UserUpdate(BaseModel):
|
class UserUpdate(BaseModel):
|
||||||
|
|||||||
Reference in New Issue
Block a user