fix(tenancy): align columns and actions in merchant team table
All checks were successful
CI / ruff (push) Successful in 15s
CI / pytest (push) Successful in 2h41m40s
CI / validate (push) Successful in 31s
CI / dependency-scanning (push) Successful in 33s
CI / docs (push) Successful in 48s
CI / deploy (push) Successful in 1m5s

- Fixed header/column alignment: Member | Role | Status | Actions
- Store count + chevron moved inline with member name (not a separate column)
- Role column shows single role, "Owner", or "Multiple roles" on main row
- Actions use fixed 4-slot grid (resend | view | edit | remove) ensuring
  icons always align vertically between main rows and sub-rows
- Empty slots render as blank space to maintain alignment

i18n: added multiple_roles key in 4 locales.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 23:39:31 +02:00
parent 4748368809
commit f81851445e
5 changed files with 70 additions and 34 deletions

View File

@@ -53,6 +53,7 @@
"personal_info": "Persönliche Informationen", "personal_info": "Persönliche Informationen",
"resend_invitation": "Einladung erneut senden", "resend_invitation": "Einladung erneut senden",
"save_profile": "Profil speichern", "save_profile": "Profil speichern",
"multiple_roles": "Mehrere Rollen",
"view_member": "Mitglied anzeigen", "view_member": "Mitglied anzeigen",
"account_information": "Kontoinformationen", "account_information": "Kontoinformationen",
"username": "Benutzername", "username": "Benutzername",

View File

@@ -53,6 +53,7 @@
"personal_info": "Personal Information", "personal_info": "Personal Information",
"resend_invitation": "Resend Invitation", "resend_invitation": "Resend Invitation",
"save_profile": "Save Profile", "save_profile": "Save Profile",
"multiple_roles": "Multiple roles",
"view_member": "View Member", "view_member": "View Member",
"account_information": "Account Information", "account_information": "Account Information",
"username": "Username", "username": "Username",

View File

@@ -53,6 +53,7 @@
"personal_info": "Informations personnelles", "personal_info": "Informations personnelles",
"resend_invitation": "Renvoyer l'invitation", "resend_invitation": "Renvoyer l'invitation",
"save_profile": "Enregistrer le profil", "save_profile": "Enregistrer le profil",
"multiple_roles": "Rôles multiples",
"view_member": "Voir le membre", "view_member": "Voir le membre",
"account_information": "Informations du compte", "account_information": "Informations du compte",
"username": "Nom d'utilisateur", "username": "Nom d'utilisateur",

View File

@@ -53,6 +53,7 @@
"personal_info": "Perséinlech Informatiounen", "personal_info": "Perséinlech Informatiounen",
"resend_invitation": "Aluedung nei schécken", "resend_invitation": "Aluedung nei schécken",
"save_profile": "Profil späicheren", "save_profile": "Profil späicheren",
"multiple_roles": "Méi Rollen",
"view_member": "Member kucken", "view_member": "Member kucken",
"account_information": "Konto Informatiounen", "account_information": "Konto Informatiounen",
"username": "Benotzernumm", "username": "Benotzernumm",

View File

@@ -78,7 +78,14 @@
<!-- Members Table --> <!-- Members Table -->
<div x-show="filteredMembers.length > 0"> <div x-show="filteredMembers.length > 0">
{% call table_wrapper() %} {% call table_wrapper() %}
{{ table_header([_('tenancy.team.member'), _('tenancy.team.stores_and_roles'), _('tenancy.team.status'), _('tenancy.team.actions')]) }} <thead>
<tr class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50 dark:text-gray-400 dark:bg-gray-800">
<th class="px-4 py-3">{{ _('tenancy.team.member') }}</th>
<th class="px-4 py-3 w-36">{{ _('tenancy.team.role') }}</th>
<th class="px-4 py-3 w-28">{{ _('tenancy.team.status') }}</th>
<th class="px-4 py-3 w-40">{{ _('tenancy.team.actions') }}</th>
</tr>
</thead>
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800"> <tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
<template x-for="member in filteredMembers" :key="member.user_id"> <template x-for="member in filteredMembers" :key="member.user_id">
<tbody class="divide-y dark:divide-gray-700"> <tbody class="divide-y dark:divide-gray-700">
@@ -94,57 +101,73 @@
<span class="text-xs font-semibold" x-text="getInitials(member)"></span> <span class="text-xs font-semibold" x-text="getInitials(member)"></span>
</div> </div>
</div> </div>
<div> <div class="flex-1">
<p class="font-semibold text-gray-800 dark:text-gray-200" <p class="font-semibold text-gray-800 dark:text-gray-200"
x-text="member.full_name"></p> x-text="member.full_name"></p>
<p class="text-xs text-gray-600 dark:text-gray-400" x-text="member.email"></p> <p class="text-xs text-gray-600 dark:text-gray-400" x-text="member.email"></p>
</div> </div>
<div class="flex items-center gap-1 ml-2 text-xs text-gray-400">
<span x-html="$icon(expandedMembers.includes(member.user_id) ? 'chevron-up' : 'chevron-down', 'w-4 h-4')"></span>
<span x-text="member.stores.length + ' store' + (member.stores.length !== 1 ? 's' : '')"></span>
</div>
</div> </div>
</td> </td>
{# Store count summary #} {# Role (summary) #}
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400"> <td class="px-4 py-3 text-sm">
<div class="flex items-center gap-1"> <template x-if="member.is_owner">
<span x-html="$icon(expandedMembers.includes(member.user_id) ? 'chevron-up' : 'chevron-down', 'w-4 h-4 text-gray-400')"></span> <span class="text-xs text-purple-600 dark:text-purple-400 font-medium">Owner</span>
<span x-text="member.stores.length + ' store' + (member.stores.length !== 1 ? 's' : '')"></span> </template>
</div> <template x-if="!member.is_owner && member.stores.length === 1">
<span class="text-xs text-gray-600 dark:text-gray-400" x-text="member.stores[0].role_name"></span>
</template>
<template x-if="!member.is_owner && member.stores.length > 1">
<span class="text-xs text-gray-400">{{ _('tenancy.team.multiple_roles') }}</span>
</template>
</td> </td>
{# Overall status #} {# Overall status #}
<td class="px-4 py-3 text-sm"> <td class="px-4 py-3 text-sm">
<template x-if="member.is_owner"> <template x-if="member.is_owner">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200"> <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200">
<span x-html="$icon('shield-check', 'w-3 h-3 mr-1')"></span> <span x-html="$icon('shield-check', 'w-3 h-3 mr-1')"></span>
{{ _('tenancy.team.owner') }} {{ _('tenancy.team.owner') }}
</span> </span>
</template> </template>
<template x-if="!member.is_owner && getMemberStatus(member) === 'active'"> <template x-if="!member.is_owner && getMemberStatus(member) === 'active'">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"> <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
{{ _('common.active') }} {{ _('common.active') }}
</span> </span>
</template> </template>
<template x-if="!member.is_owner && getMemberStatus(member) === 'pending'"> <template x-if="!member.is_owner && getMemberStatus(member) === 'pending'">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200"> <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200">
{{ _('common.pending') }} {{ _('common.pending') }}
</span> </span>
</template> </template>
</td> </td>
{# Member-level actions #} {# Member-level actions: fixed 4-slot grid (resend | view | edit | remove) #}
<td class="px-4 py-3 text-sm"> <td class="px-4 py-3 text-sm">
<div class="flex items-center gap-2" @click.stop> <div class="grid grid-cols-4 gap-1 w-32" @click.stop>
{# Slot 1: resend (empty at member level) #}
<span></span>
{# Slot 2: view #}
<button @click="openViewModal(member)" <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" class="p-1 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 transition-colors"
:title="$t('tenancy.team.view_member')"> :title="$t('tenancy.team.view_member')">
<span x-html="$icon('eye', 'w-4 h-4')"></span> <span x-html="$icon('eye', 'w-4 h-4')"></span>
</button> </button>
{# Slot 3: edit #}
<template x-if="!member.is_owner"> <template x-if="!member.is_owner">
<button @click="openEditModal(member)" <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" class="p-1 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 transition-colors"
:title="$t('tenancy.team.edit_member')"> :title="$t('tenancy.team.edit_member')">
<span x-html="$icon('pencil', 'w-4 h-4')"></span> <span x-html="$icon('pencil', 'w-4 h-4')"></span>
</button> </button>
</template> </template>
<template x-if="member.is_owner"><span></span></template>
{# Slot 4: remove (empty at member level — done per-store) #}
<span></span>
</div> </div>
</td> </td>
</tr> </tr>
@@ -154,10 +177,10 @@
<tr x-show="expandedMembers.includes(member.user_id)" <tr x-show="expandedMembers.includes(member.user_id)"
x-transition x-transition
class="bg-gray-50 dark:bg-gray-900/50 text-gray-600 dark:text-gray-400"> class="bg-gray-50 dark:bg-gray-900/50 text-gray-600 dark:text-gray-400">
{# Indent + Store name #} {# Store name (indented) #}
<td class="px-4 py-2 pl-16"> <td class="px-4 py-2 pl-16">
<div class="flex items-center gap-2 text-sm"> <div class="flex items-center gap-2 text-sm">
<span x-html="$icon('shopping-bag', 'w-4 h-4 text-gray-400')"></span> <span x-html="$icon('shopping-bag', 'w-3.5 h-3.5 text-gray-400')"></span>
<span class="font-medium text-gray-700 dark:text-gray-300" x-text="store.store_name"></span> <span class="font-medium text-gray-700 dark:text-gray-300" x-text="store.store_name"></span>
<span class="text-xs text-gray-400 font-mono" x-text="store.store_code"></span> <span class="text-xs text-gray-400 font-mono" x-text="store.store_code"></span>
</div> </div>
@@ -183,24 +206,33 @@
</template> </template>
</td> </td>
{# Per-store actions #} {# Per-store actions: same 4-slot grid (resend | view | edit | remove) #}
<td class="px-4 py-2 text-sm"> <td class="px-4 py-2 text-sm">
<div class="flex items-center gap-2" x-show="!member.is_owner"> <div class="grid grid-cols-4 gap-1 w-32">
{# Resend pending only #} {# Slot 1: resend (pending only) #}
<button x-show="store.is_pending" <template x-if="store.is_pending && !member.is_owner">
@click="resendStoreInvitation(store.store_id, member.user_id)" <button @click="resendStoreInvitation(store.store_id, member.user_id)"
:disabled="saving" :disabled="saving"
class="p-1 text-gray-400 hover:text-green-600 dark:hover:text-green-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors" class="p-1 text-gray-400 hover:text-green-600 dark:hover:text-green-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors"
:title="$t('tenancy.team.resend_invitation')"> :title="$t('tenancy.team.resend_invitation')">
<span x-html="$icon('paper-airplane', 'w-3.5 h-3.5')"></span> <span x-html="$icon('paper-airplane', 'w-3.5 h-3.5')"></span>
</button> </button>
{# Remove from this store #} </template>
<template x-if="!store.is_pending || member.is_owner"><span></span></template>
{# Slot 2: view (empty at store level) #}
<span></span>
{# Slot 3: edit (empty at store level) #}
<span></span>
{# Slot 4: remove from store #}
<template x-if="!member.is_owner">
<button @click="removeMember(store.store_id, member.user_id)" <button @click="removeMember(store.store_id, member.user_id)"
:disabled="saving" :disabled="saving"
class="p-1 text-gray-400 hover:text-red-600 dark:hover:text-red-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors" class="p-1 text-gray-400 hover:text-red-600 dark:hover:text-red-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors"
:title="$t('tenancy.team.remove_member')"> :title="$t('tenancy.team.remove_member')">
<span x-html="$icon('x-circle', 'w-3.5 h-3.5')"></span> <span x-html="$icon('x-circle', 'w-3.5 h-3.5')"></span>
</button> </button>
</template>
<template x-if="member.is_owner"><span></span></template>
</div> </div>
</td> </td>
</tr> </tr>