feat(tenancy): add member detail modal + fix invite name saving
Some checks failed
CI / ruff (push) Successful in 15s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled

Merchant team page:
- Consistent member display (full_name + email on every row)
- New view button (eye icon) on all members including owner
- View modal shows account info (username, role, email verified,
  last login, account created) and store memberships with roles
- API enriched with user metadata (username, role, is_email_verified,
  last_login, created_at)

Invite fix (both merchant and store routes):
- first_name and last_name from invite form were never passed to the
  service that creates the User account. Now passed through correctly.

i18n: 6 new keys across 4 locales (view_member, account_information,
username, email_verified, last_login, account_created).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 13:23:20 +02:00
parent d81e9a3fa4
commit 211c46ebbc
10 changed files with 151 additions and 26 deletions

View File

@@ -92,13 +92,9 @@
</div>
</div>
<div>
<p class="font-semibold text-gray-800 dark:text-gray-200">
<span x-text="member.first_name || ''"></span>
<span x-text="member.last_name || ''"></span>
<span x-show="!member.first_name && !member.last_name" x-text="member.email"></span>
</p>
<p class="text-xs text-gray-600 dark:text-gray-400" x-text="member.email"
x-show="member.first_name || member.last_name"></p>
<p class="font-semibold text-gray-800 dark:text-gray-200"
x-text="member.full_name"></p>
<p class="text-xs text-gray-600 dark:text-gray-400" x-text="member.email"></p>
</div>
</div>
</td>
@@ -142,25 +138,32 @@
<!-- Actions -->
<td class="px-4 py-3 text-sm">
<template x-if="member.is_owner">
<span class="inline-flex items-center px-2 py-1 text-xs text-purple-600 dark:text-purple-400">
<span x-html="$icon('shield-check', 'w-4 h-4')"></span>
</span>
</template>
<template x-if="!member.is_owner">
<div class="flex items-center gap-2">
<button @click="openEditModal(member)"
class="p-1.5 text-gray-500 hover:text-purple-600 dark:text-gray-400 dark:hover:text-purple-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
:title="$t('tenancy.team.edit_member')">
<span x-html="$icon('pencil', 'w-4 h-4')"></span>
</button>
<button @click="openRemoveModal(member)"
class="p-1.5 text-gray-500 hover:text-red-600 dark:text-gray-400 dark:hover:text-red-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
:title="$t('tenancy.team.remove_member')">
<span x-html="$icon('trash', 'w-4 h-4')"></span>
</button>
</div>
</template>
<div class="flex items-center gap-2">
<button @click="openViewModal(member)"
class="p-1.5 text-gray-500 hover:text-blue-600 dark:text-gray-400 dark:hover:text-blue-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
:title="$t('tenancy.team.view_member')">
<span x-html="$icon('eye', 'w-4 h-4')"></span>
</button>
<template x-if="member.is_owner">
<span class="inline-flex items-center px-2 py-1 text-xs text-purple-600 dark:text-purple-400">
<span x-html="$icon('shield-check', 'w-4 h-4')"></span>
</span>
</template>
<template x-if="!member.is_owner">
<div class="flex items-center gap-2">
<button @click="openEditModal(member)"
class="p-1.5 text-gray-500 hover:text-purple-600 dark:text-gray-400 dark:hover:text-purple-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
:title="$t('tenancy.team.edit_member')">
<span x-html="$icon('pencil', 'w-4 h-4')"></span>
</button>
<button @click="openRemoveModal(member)"
class="p-1.5 text-gray-500 hover:text-red-600 dark:text-gray-400 dark:hover:text-red-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
:title="$t('tenancy.team.remove_member')">
<span x-html="$icon('trash', 'w-4 h-4')"></span>
</button>
</div>
</template>
</div>
</td>
</tr>
</template>
@@ -372,6 +375,79 @@
</div>
{% endcall %}
<!-- ==================== VIEW MEMBER MODAL ==================== -->
{% call modal('viewModal', _('tenancy.team.view_member'), 'showViewModal', size='md', show_footer=false) %}
<div x-show="selectedMember" class="space-y-6">
<!-- Member header -->
<div class="flex items-center gap-4 pb-4 border-b border-gray-200 dark:border-gray-700">
<div class="w-14 h-14 rounded-full bg-purple-100 dark:bg-purple-900 flex items-center justify-center">
<span class="text-lg font-semibold text-purple-600 dark:text-purple-300"
x-text="selectedMember ? getInitials(selectedMember) : ''"></span>
</div>
<div>
<p class="text-lg font-semibold text-gray-900 dark:text-white"
x-text="selectedMember?.full_name"></p>
<p class="text-sm text-gray-500 dark:text-gray-400" x-text="selectedMember?.email"></p>
</div>
</div>
<!-- Account Information -->
<div>
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">{{ _('tenancy.team.account_information') }}</h4>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">{{ _('tenancy.team.username') }}</label>
<p class="text-sm font-mono text-gray-900 dark:text-gray-100" x-text="selectedMember?.username || '-'"></p>
</div>
<div>
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">{{ _('tenancy.team.role') }}</label>
<p class="text-sm text-gray-900 dark:text-gray-100" x-text="selectedMember?.role || '-'"></p>
</div>
<div>
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">{{ _('tenancy.team.email_verified') }}</label>
<span :class="selectedMember?.is_email_verified
? 'px-2 py-0.5 text-xs font-semibold text-green-700 bg-green-100 rounded-full dark:bg-green-700 dark:text-green-100'
: 'px-2 py-0.5 text-xs font-semibold text-gray-700 bg-gray-100 rounded-full dark:bg-gray-700 dark:text-gray-100'"
x-text="selectedMember?.is_email_verified ? 'Verified' : 'Not verified'"></span>
</div>
<div>
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">{{ _('tenancy.team.last_login') }}</label>
<p class="text-sm text-gray-900 dark:text-gray-100" x-text="selectedMember?.last_login ? formatDate(selectedMember.last_login) : 'Never'"></p>
</div>
<div>
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">{{ _('tenancy.team.account_created') }}</label>
<p class="text-sm text-gray-900 dark:text-gray-100" x-text="selectedMember?.created_at ? formatDate(selectedMember.created_at) : '-'"></p>
</div>
</div>
</div>
<!-- Store Memberships -->
<div>
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">{{ _('tenancy.team.store_roles') }}</h4>
<div class="space-y-2">
<template x-for="store in selectedMember?.stores || []" :key="store.store_id">
<div class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<div>
<p class="text-sm font-medium text-gray-800 dark:text-gray-200" x-text="store.store_name"></p>
<p class="text-xs text-gray-400 font-mono" x-text="store.store_code"></p>
</div>
<span class="px-2 py-1 text-xs rounded-full bg-purple-100 dark:bg-purple-900 text-purple-700 dark:text-purple-300"
x-text="store.role_name || 'Owner'"></span>
</div>
</template>
</div>
</div>
<!-- Close button -->
<div class="flex justify-end pt-4 border-t border-gray-200 dark:border-gray-700">
<button @click="showViewModal = false"
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors">
{{ _('common.close') }}
</button>
</div>
</div>
{% endcall %}
{% endblock %}
{% block extra_scripts %}