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",
"resend_invitation": "Einladung erneut senden",
"save_profile": "Profil speichern",
"multiple_roles": "Mehrere Rollen",
"view_member": "Mitglied anzeigen",
"account_information": "Kontoinformationen",
"username": "Benutzername",

View File

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

View File

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

View File

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

View File

@@ -78,7 +78,14 @@
<!-- Members Table -->
<div x-show="filteredMembers.length > 0">
{% 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">
<template x-for="member in filteredMembers" :key="member.user_id">
<tbody class="divide-y dark:divide-gray-700">
@@ -94,57 +101,73 @@
<span class="text-xs font-semibold" x-text="getInitials(member)"></span>
</div>
</div>
<div>
<div class="flex-1">
<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 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>
</td>
{# Store count summary #}
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
<div class="flex items-center gap-1">
<span x-html="$icon(expandedMembers.includes(member.user_id) ? 'chevron-up' : 'chevron-down', 'w-4 h-4 text-gray-400')"></span>
<span x-text="member.stores.length + ' store' + (member.stores.length !== 1 ? 's' : '')"></span>
</div>
{# Role (summary) #}
<td class="px-4 py-3 text-sm">
<template x-if="member.is_owner">
<span class="text-xs text-purple-600 dark:text-purple-400 font-medium">Owner</span>
</template>
<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>
{# Overall status #}
<td class="px-4 py-3 text-sm">
<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>
{{ _('tenancy.team.owner') }}
</span>
</template>
<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') }}
</span>
</template>
<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') }}
</span>
</template>
</td>
{# Member-level actions #}
{# Member-level actions: fixed 4-slot grid (resend | view | edit | remove) #}
<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)"
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')">
<span x-html="$icon('eye', 'w-4 h-4')"></span>
</button>
{# Slot 3: edit #}
<template x-if="!member.is_owner">
<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')">
<span x-html="$icon('pencil', 'w-4 h-4')"></span>
</button>
</template>
<template x-if="member.is_owner"><span></span></template>
{# Slot 4: remove (empty at member level — done per-store) #}
<span></span>
</div>
</td>
</tr>
@@ -154,10 +177,10 @@
<tr x-show="expandedMembers.includes(member.user_id)"
x-transition
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">
<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="text-xs text-gray-400 font-mono" x-text="store.store_code"></span>
</div>
@@ -183,24 +206,33 @@
</template>
</td>
{# Per-store actions #}
{# Per-store actions: same 4-slot grid (resend | view | edit | remove) #}
<td class="px-4 py-2 text-sm">
<div class="flex items-center gap-2" x-show="!member.is_owner">
{# Resend pending only #}
<button x-show="store.is_pending"
@click="resendStoreInvitation(store.store_id, member.user_id)"
<div class="grid grid-cols-4 gap-1 w-32">
{# Slot 1: resend (pending only) #}
<template x-if="store.is_pending && !member.is_owner">
<button @click="resendStoreInvitation(store.store_id, member.user_id)"
: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"
:title="$t('tenancy.team.resend_invitation')">
<span x-html="$icon('paper-airplane', 'w-3.5 h-3.5')"></span>
</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)"
: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"
:title="$t('tenancy.team.remove_member')">
<span x-html="$icon('x-circle', 'w-3.5 h-3.5')"></span>
</button>
</template>
<template x-if="member.is_owner"><span></span></template>
</div>
</td>
</tr>