refactor(MembersOverviewSection): simplify layout and enhance filtering options

- Removed unnecessary subtitle and member statistics display for a cleaner interface.
- Introduced a collapsible section for advanced filters, improving user experience by organizing search and filter options.
- Maintained existing functionality while enhancing the overall layout and accessibility of member filtering features.
This commit is contained in:
Torsten Schulz (local)
2026-03-18 18:07:01 +01:00
parent 9340ee3509
commit bf40927efb

View File

@@ -3,7 +3,6 @@
<div class="members-header">
<div>
<h2>{{ $t('members.title') }}</h2>
<p class="members-subtitle">{{ $t('members.subtitle') }}</p>
</div>
<div class="action-buttons">
<button @click="$emit('toggle-new-member')" class="btn-primary">
@@ -19,38 +18,8 @@
</div>
</div>
<div class="members-stats-grid">
<div class="members-stat-card">
<span class="members-stat-label">{{ $t('members.activeMembers') }}</span>
<strong class="members-stat-value">{{ activeMembersCount }}</strong>
</div>
<div class="members-stat-card">
<span class="members-stat-label">{{ $t('members.testMembers') }}</span>
<strong class="members-stat-value">{{ testMembersCount }}</strong>
</div>
<div class="members-stat-card">
<span class="members-stat-label">{{ $t('members.inactiveMembers') }}</span>
<strong class="members-stat-value">{{ inactiveMembersCount }}</strong>
</div>
<div class="members-stat-card members-stat-card-accent">
<span class="members-stat-label">{{ $t('members.visibleMembers') }}</span>
<strong class="members-stat-value">{{ visibleMembersCount }}</strong>
</div>
</div>
<div class="filters-section">
<div class="members-filter-topline">
<div class="member-search-group">
<label for="member-search-input">{{ $t('members.search') }}</label>
<input
id="member-search-input"
:value="searchQuery"
type="search"
class="member-search-input"
:placeholder="$t('members.searchPlaceholder')"
@input="$emit('update:search-query', $event.target.value.trim())"
>
</div>
<div class="members-filter-topline members-filter-topline-compact">
<div class="members-scope-buttons" role="tablist" :aria-label="$t('members.memberScope')">
<button
v-for="scope in memberScopeOptions"
@@ -66,71 +35,92 @@
</div>
</div>
<div class="filter-controls">
<div class="checkbox-group">
<label class="checkbox-item">
<details class="members-advanced-filters">
<summary>Suche und Filter</summary>
<div class="filter-controls">
<div class="member-search-group">
<label for="member-search-input">{{ $t('members.search') }}</label>
<input
type="checkbox"
:checked="showInactiveMembers"
@change="$emit('update:show-inactive-members', $event.target.checked)"
>
<span>{{ $t('members.showInactiveMembers') }}</span>
</label>
</div>
<div class="filter-group">
<label>{{ $t('members.ageGroup') }}:</label>
<select :value="selectedAgeGroup" class="filter-select" @change="$emit('update:selected-age-group', $event.target.value)">
<option value="">{{ $t('common.all') }}</option>
<option value="adult">{{ $t('members.adults') }}</option>
<option value="J19">{{ $t('members.j19') }}</option>
<option value="J17">{{ $t('members.j17') }}</option>
<option value="J15">{{ $t('members.j15') }}</option>
<option value="J13">{{ $t('members.j13') }}</option>
<option value="J11">{{ $t('members.j11') }}</option>
<option value="range">Alter von - bis</option>
</select>
</div>
<div v-if="selectedAgeGroup === 'range'" class="filter-group age-range-group">
<label>Alter von - bis:</label>
<div class="age-range-inputs">
<input
:value="selectedAgeFrom"
type="number"
min="0"
max="120"
class="filter-select age-range-input"
placeholder="von"
@input="$emit('update:selected-age-from', $event.target.value)"
>
<span class="age-range-separator">-</span>
<input
:value="selectedAgeTo"
type="number"
min="0"
max="120"
class="filter-select age-range-input"
placeholder="bis"
@input="$emit('update:selected-age-to', $event.target.value)"
id="member-search-input"
:value="searchQuery"
type="search"
class="member-search-input"
:placeholder="$t('members.searchPlaceholder')"
@input="$emit('update:search-query', $event.target.value.trim())"
>
</div>
<div class="checkbox-group">
<label class="checkbox-item">
<input
type="checkbox"
:checked="showInactiveMembers"
@change="$emit('update:show-inactive-members', $event.target.checked)"
>
<span>{{ $t('members.showInactiveMembers') }}</span>
</label>
</div>
<div class="filter-group">
<label>{{ $t('members.ageGroup') }}:</label>
<select :value="selectedAgeGroup" class="filter-select" @change="$emit('update:selected-age-group', $event.target.value)">
<option value="">{{ $t('common.all') }}</option>
<option value="adult">{{ $t('members.adults') }}</option>
<option value="J19">{{ $t('members.j19') }}</option>
<option value="J17">{{ $t('members.j17') }}</option>
<option value="J15">{{ $t('members.j15') }}</option>
<option value="J13">{{ $t('members.j13') }}</option>
<option value="J11">{{ $t('members.j11') }}</option>
<option value="range">Alter von - bis</option>
</select>
</div>
<div v-if="selectedAgeGroup === 'range'" class="filter-group age-range-group">
<label>Alter von - bis:</label>
<div class="age-range-inputs">
<input
:value="selectedAgeFrom"
type="number"
min="0"
max="120"
class="filter-select age-range-input"
placeholder="von"
@input="$emit('update:selected-age-from', $event.target.value)"
>
<span class="age-range-separator">-</span>
<input
:value="selectedAgeTo"
type="number"
min="0"
max="120"
class="filter-select age-range-input"
placeholder="bis"
@input="$emit('update:selected-age-to', $event.target.value)"
>
</div>
</div>
<div class="filter-group">
<label>{{ $t('members.gender') }}:</label>
<select :value="selectedGender" class="filter-select" @change="$emit('update:selected-gender', $event.target.value)">
<option value="">{{ $t('common.all') }}</option>
<option value="female">{{ $t('members.genderFemale') }}</option>
<option value="male">{{ $t('members.genderMale') }}</option>
<option value="diverse">{{ $t('members.genderDiverse') }}</option>
<option value="unknown">{{ $t('members.genderUnknown') }}</option>
</select>
</div>
<button @click="$emit('clear-filters')" class="btn-clear-filters">{{ $t('members.clearFilters') }}</button>
</div>
<div class="filter-group">
<label>{{ $t('members.gender') }}:</label>
<select :value="selectedGender" class="filter-select" @change="$emit('update:selected-gender', $event.target.value)">
<option value="">{{ $t('common.all') }}</option>
<option value="female">{{ $t('members.genderFemale') }}</option>
<option value="male">{{ $t('members.genderMale') }}</option>
<option value="diverse">{{ $t('members.genderDiverse') }}</option>
<option value="unknown">{{ $t('members.genderUnknown') }}</option>
</select>
</div>
<button @click="$emit('clear-filters')" class="btn-clear-filters">{{ $t('members.clearFilters') }}</button>
</div>
</details>
</div>
<div class="members-results-bar">
<div class="members-results-copy">
<strong>{{ visibleMembersCount }}</strong> {{ $t('members.resultsVisible') }}
<div class="members-results-summary">
<div class="members-results-copy">
<strong>{{ visibleMembersCount }}</strong> {{ $t('members.resultsVisible') }}
</div>
<div class="members-results-stats">
<span class="results-stat-pill">{{ $t('members.activeMembers') }}: {{ activeMembersCount }}</span>
<span class="results-stat-pill">{{ $t('members.testMembers') }}: {{ testMembersCount }}</span>
<span class="results-stat-pill">{{ $t('members.inactiveMembers') }}: {{ inactiveMembersCount }}</span>
</div>
</div>
<div class="members-results-actions">
<label class="members-sort-group">
@@ -147,6 +137,12 @@
<button type="button" class="member-icon-button" :title="$t('members.toggleSortDirection')" @click="$emit('toggle-sort-direction')">
{{ sortDirection === 'asc' ? '' : '' }}
</button>
<div class="members-results-hint">{{ $t('members.editHint') }}</div>
</div>
</div>
<details class="members-bulk-actions">
<summary>Sammelaktionen</summary>
<div class="members-bulk-actions-grid">
<button type="button" @click="$emit('create-phone-list-for-filtered')" :disabled="visibleMembersCount === 0">
{{ $t('members.phoneListForSelection') }}
</button>
@@ -156,56 +152,26 @@
<button type="button" @click="$emit('mark-filtered-as-regular')" :disabled="filteredTestMembersCount === 0">
{{ $t('members.markRegularForSelection') }}
</button>
<div class="members-results-hint">{{ $t('members.editHint') }}</div>
<button type="button" @click="$emit('export-filtered-members-csv')" :disabled="visibleMembersCount === 0">
{{ $t('members.exportCsv') }}
</button>
<button type="button" @click="$emit('copy-filtered-phones')" :disabled="filteredMembersWithPhonesCount === 0">
{{ $t('members.copyPhones') }}
</button>
<button type="button" @click="$emit('copy-filtered-emails')" :disabled="filteredMembersWithEmailsCount === 0">
{{ $t('members.copyEmails') }}
</button>
<button type="button" @click="$emit('compose-email-to-filtered')" :disabled="filteredMembersWithEmailsCount === 0">
{{ $t('members.composeEmail') }}
</button>
<button type="button" @click="$emit('copy-filtered-contact-summary')" :disabled="visibleMembersCount === 0">
{{ $t('members.copyContactSummary') }}
</button>
</div>
</div>
<div class="members-export-preview">
<div class="members-export-card">
<span class="members-export-label">{{ $t('members.exportPreview') }}</span>
<strong class="members-export-value">{{ visibleMembersCount }}</strong>
<span class="members-export-copy">{{ $t('members.exportMembersSelected') }}</span>
<div class="members-export-actions">
<button type="button" @click="$emit('export-filtered-members-csv')" :disabled="visibleMembersCount === 0">
{{ $t('members.exportCsv') }}
</button>
</div>
<div class="members-bulk-preview">
{{ exportPreviewNames.length ? exportPreviewNames.join(', ') : $t('members.exportPreviewEmpty') }}
</div>
<div class="members-export-card">
<span class="members-export-label">{{ $t('members.exportPhones') }}</span>
<strong class="members-export-value">{{ filteredMembersWithPhonesCount }}</strong>
<span class="members-export-copy">{{ $t('members.exportReachableByPhone') }}</span>
<div class="members-export-actions">
<button type="button" @click="$emit('copy-filtered-phones')" :disabled="filteredMembersWithPhonesCount === 0">
{{ $t('members.copyPhones') }}
</button>
</div>
</div>
<div class="members-export-card">
<span class="members-export-label">{{ $t('members.exportEmails') }}</span>
<strong class="members-export-value">{{ filteredMembersWithEmailsCount }}</strong>
<span class="members-export-copy">{{ $t('members.exportReachableByEmail') }}</span>
<div class="members-export-actions">
<button type="button" @click="$emit('copy-filtered-emails')" :disabled="filteredMembersWithEmailsCount === 0">
{{ $t('members.copyEmails') }}
</button>
<button type="button" @click="$emit('compose-email-to-filtered')" :disabled="filteredMembersWithEmailsCount === 0">
{{ $t('members.composeEmail') }}
</button>
</div>
</div>
<div class="members-export-card members-export-card-wide">
<span class="members-export-label">{{ $t('members.exportPreviewNames') }}</span>
<span class="members-export-copy">
{{ exportPreviewNames.length ? exportPreviewNames.join(', ') : $t('members.exportPreviewEmpty') }}
</span>
<div class="members-export-actions">
<button type="button" @click="$emit('copy-filtered-contact-summary')" :disabled="visibleMembersCount === 0">
{{ $t('members.copyContactSummary') }}
</button>
</div>
</div>
</div>
</details>
</div>
</template>
@@ -279,11 +245,6 @@ export default {
flex-wrap: wrap;
}
.members-subtitle {
margin: 0.2rem 0 0;
color: var(--text-muted);
}
.action-buttons,
.filter-controls,
.members-results-actions,
@@ -295,48 +256,26 @@ export default {
align-items: center;
}
.members-stats-grid,
.members-export-preview {
display: grid;
gap: 0.85rem;
}
.members-stats-grid {
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.members-export-preview {
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
.members-stat-card,
.members-export-card,
.filters-section,
.members-results-bar {
.members-results-bar,
.members-bulk-actions {
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
background: var(--background-light);
padding: 0.85rem;
}
.members-stat-card-accent {
background: linear-gradient(135deg, var(--primary-soft), var(--background-light));
.filters-section,
.members-results-bar {
padding-top: 0.7rem;
padding-bottom: 0.7rem;
}
.members-stat-label,
.members-export-label,
.members-results-hint {
font-size: 0.8rem;
color: var(--text-muted);
}
.members-stat-value,
.members-export-value {
display: block;
margin-top: 0.2rem;
font-size: 1.3rem;
}
.member-search-group,
.members-sort-group,
.filter-group,
@@ -415,8 +354,68 @@ export default {
color: var(--primary-strong);
}
.members-export-card-wide {
grid-column: 1 / -1;
.members-filter-topline-compact {
align-items: center;
}
.members-advanced-filters {
margin-top: 0.75rem;
border-top: 1px solid var(--border-color);
padding-top: 0.75rem;
}
.members-advanced-filters summary {
cursor: pointer;
font-weight: 600;
color: var(--text-primary);
user-select: none;
}
.members-advanced-filters .filter-controls {
margin-top: 0.85rem;
}
.members-results-summary {
display: flex;
flex-direction: column;
gap: 0.45rem;
}
.members-results-stats {
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
}
.results-stat-pill {
display: inline-flex;
align-items: center;
padding: 0.2rem 0.55rem;
border-radius: 999px;
background: #eef2f5;
color: #475569;
font-size: 0.78rem;
font-weight: 600;
}
.members-bulk-actions summary {
cursor: pointer;
font-weight: 600;
color: var(--text-primary);
user-select: none;
}
.members-bulk-actions-grid {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
margin-top: 0.85rem;
}
.members-bulk-preview {
margin-top: 0.75rem;
color: var(--text-muted);
font-size: 0.82rem;
}
@media (max-width: 900px) {