feat: Add search and selection to Users view

- Search by name, email, phone
- Checkbox selection for bulk actions
- Select all / clear selection
- Bulk activate/deactivate
- Click row to edit user
- Edit modal for user details
- Added Subunternehmer role option
This commit is contained in:
2026-03-13 15:09:43 +00:00
parent 3680315040
commit 6fe7b7ec22

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { api } from '@/api' import { api } from '@/api'
@@ -20,6 +20,11 @@ interface User {
const users = ref<User[]>([]) const users = ref<User[]>([])
const loading = ref(true) const loading = ref(true)
const showCreateModal = ref(false) const showCreateModal = ref(false)
const searchQuery = ref('')
const selectedUsers = ref<string[]>([])
const showEditModal = ref(false)
const editingUser = ref<User | null>(null)
const newUser = ref({ const newUser = ref({
email: '', email: '',
password: '', password: '',
@@ -29,6 +34,22 @@ const newUser = ref({
role: 'mitarbeiter' as const role: 'mitarbeiter' as const
}) })
const filteredUsers = computed(() => {
if (!searchQuery.value) return users.value
const query = searchQuery.value.toLowerCase()
return users.value.filter(u =>
u.first_name.toLowerCase().includes(query) ||
u.last_name.toLowerCase().includes(query) ||
u.email.toLowerCase().includes(query) ||
u.phone?.toLowerCase().includes(query)
)
})
const allSelected = computed(() =>
filteredUsers.value.length > 0 &&
filteredUsers.value.every(u => selectedUsers.value.includes(u.id))
)
onMounted(async () => { onMounted(async () => {
await loadUsers() await loadUsers()
}) })
@@ -69,11 +90,78 @@ async function toggleActive(user: User) {
} }
} }
function toggleSelection(userId: string) {
const idx = selectedUsers.value.indexOf(userId)
if (idx > -1) {
selectedUsers.value.splice(idx, 1)
} else {
selectedUsers.value.push(userId)
}
}
function toggleSelectAll() {
if (allSelected.value) {
selectedUsers.value = []
} else {
selectedUsers.value = filteredUsers.value.map(u => u.id)
}
}
function clearSelection() {
selectedUsers.value = []
}
function openEditModal(user: User) {
editingUser.value = { ...user }
showEditModal.value = true
}
async function saveUser() {
if (!editingUser.value) return
try {
await api.put(`/users/${editingUser.value.id}`, {
first_name: editingUser.value.first_name,
last_name: editingUser.value.last_name,
phone: editingUser.value.phone,
role: editingUser.value.role
})
showEditModal.value = false
editingUser.value = null
await loadUsers()
} catch (e) {
alert(e instanceof Error ? e.message : t('messages.error'))
}
}
// Bulk actions (placeholder for future)
async function bulkActivate() {
for (const userId of selectedUsers.value) {
const user = users.value.find(u => u.id === userId)
if (user && !user.active) {
await api.put(`/users/${userId}`, { active: true })
}
}
selectedUsers.value = []
await loadUsers()
}
async function bulkDeactivate() {
for (const userId of selectedUsers.value) {
const user = users.value.find(u => u.id === userId)
if (user && user.active && user.role !== 'chef') {
await api.delete(`/users/${userId}`)
}
}
selectedUsers.value = []
await loadUsers()
}
function getRoleBadge(role: string) { function getRoleBadge(role: string) {
const badges: Record<string, string> = { const badges: Record<string, string> = {
chef: 'badge-danger', chef: 'badge-danger',
disponent: 'badge-primary', disponent: 'badge-primary',
mitarbeiter: 'badge-success' mitarbeiter: 'badge-success',
subunternehmer: 'badge-warning'
} }
return badges[role] || 'badge-secondary' return badges[role] || 'badge-secondary'
} }
@@ -90,27 +178,116 @@ function getRoleLabel(role: string) {
<button class="btn btn-primary" @click="showCreateModal = true">+ {{ t('users.new') }}</button> <button class="btn btn-primary" @click="showCreateModal = true">+ {{ t('users.new') }}</button>
</div> </div>
<!-- Search and Filters -->
<div class="card">
<div class="flex flex-col md:flex-row gap-4">
<div class="flex-1 relative">
<input
v-model="searchQuery"
type="text"
:placeholder="'🔍 ' + t('app.search') + '...'"
class="input w-full pl-4"
/>
<button
v-if="searchQuery"
@click="searchQuery = ''"
class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
</button>
</div>
<div class="text-sm text-gray-500 flex items-center">
{{ filteredUsers.length }} {{ t('users.employees') }}
</div>
</div>
</div>
<!-- Selection Action Bar -->
<div v-if="selectedUsers.length > 0" class="card bg-primary-50 dark:bg-primary-900/20 border-primary-200 dark:border-primary-800">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="font-medium text-primary-700 dark:text-primary-300">
{{ selectedUsers.length }} {{ t('app.add') }}
</span>
<button @click="clearSelection" class="text-sm text-gray-500 hover:text-gray-700">
{{ t('app.cancel') }}
</button>
</div>
<div class="flex gap-2">
<button @click="bulkActivate" class="btn btn-success text-sm">
{{ t('modules.enable') }}
</button>
<button @click="bulkDeactivate" class="btn btn-danger text-sm">
{{ t('modules.disable') }}
</button>
</div>
</div>
</div>
<!-- Users Table -->
<div class="card"> <div class="card">
<div v-if="loading" class="text-center py-8 text-gray-500">{{ t('app.loading') }}</div> <div v-if="loading" class="text-center py-8 text-gray-500">{{ t('app.loading') }}</div>
<div v-else-if="users.length === 0" class="text-center py-8 text-gray-500">{{ t('messages.noData') }}</div> <div v-else-if="filteredUsers.length === 0" class="text-center py-8 text-gray-500">
<p class="text-4xl mb-2">👥</p>
<p>{{ t('messages.noData') }}</p>
</div>
<table v-else class="w-full"> <table v-else class="w-full">
<thead> <thead>
<tr class="text-left text-sm text-gray-500 border-b border-gray-200 dark:border-gray-700"> <tr class="text-left text-sm text-gray-500 border-b border-gray-200 dark:border-gray-700">
<th class="pb-3 w-10">
<input
type="checkbox"
:checked="allSelected"
@change="toggleSelectAll"
class="w-4 h-4 rounded border-gray-300"
/>
</th>
<th class="pb-3">{{ t('auth.firstName') }} {{ t('auth.lastName') }}</th> <th class="pb-3">{{ t('auth.firstName') }} {{ t('auth.lastName') }}</th>
<th class="pb-3">{{ t('auth.email') }}</th> <th class="pb-3 hidden md:table-cell">{{ t('auth.email') }}</th>
<th class="pb-3">{{ t('users.role') }}</th> <th class="pb-3">{{ t('users.role') }}</th>
<th class="pb-3">{{ t('app.status') }}</th> <th class="pb-3">{{ t('app.status') }}</th>
<th class="pb-3"></th> <th class="pb-3"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="user in users" :key="user.id" class="border-b border-gray-100 dark:border-gray-800"> <tr
<td class="py-3 font-medium">{{ user.first_name }} {{ user.last_name }}</td> v-for="user in filteredUsers"
<td class="py-3 text-gray-500">{{ user.email }}</td> :key="user.id"
<td class="py-3"><span :class="['badge', getRoleBadge(user.role)]">{{ getRoleLabel(user.role) }}</span></td> class="border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer"
<td class="py-3"><span :class="user.active ? 'text-green-600' : 'text-red-600'">{{ user.active ? t('users.active') : t('users.inactive') }}</span></td> @click="openEditModal(user)"
<td class="py-3 text-right"> >
<td class="py-3" @click.stop>
<input
type="checkbox"
:checked="selectedUsers.includes(user.id)"
@change="toggleSelection(user.id)"
class="w-4 h-4 rounded border-gray-300"
/>
</td>
<td class="py-3">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center flex-shrink-0">
<span class="text-primary-600 dark:text-primary-300 text-sm font-medium">
{{ user.first_name?.[0] }}{{ user.last_name?.[0] }}
</span>
</div>
<div>
<p class="font-medium">{{ user.first_name }} {{ user.last_name }}</p>
<p class="text-xs text-gray-500 md:hidden">{{ user.email }}</p>
</div>
</div>
</td>
<td class="py-3 text-gray-500 hidden md:table-cell">{{ user.email }}</td>
<td class="py-3">
<span :class="['badge', getRoleBadge(user.role)]">{{ getRoleLabel(user.role) }}</span>
</td>
<td class="py-3">
<span :class="user.active ? 'text-green-600' : 'text-red-600'">
{{ user.active ? '✓' : '✗' }}
</span>
</td>
<td class="py-3 text-right" @click.stop>
<button <button
v-if="user.id !== authStore.user?.id && user.role !== 'chef'" v-if="user.id !== authStore.user?.id && user.role !== 'chef'"
class="text-sm text-gray-500 hover:text-red-600" class="text-sm text-gray-500 hover:text-red-600"
@@ -156,6 +333,7 @@ function getRoleLabel(role: string) {
<select v-model="newUser.role" class="input"> <select v-model="newUser.role" class="input">
<option value="mitarbeiter">{{ t('users.roles.mitarbeiter') }}</option> <option value="mitarbeiter">{{ t('users.roles.mitarbeiter') }}</option>
<option value="disponent">{{ t('users.roles.disponent') }}</option> <option value="disponent">{{ t('users.roles.disponent') }}</option>
<option value="subunternehmer">Subunternehmer</option>
</select> </select>
</div> </div>
<div class="flex justify-end gap-3 pt-4"> <div class="flex justify-end gap-3 pt-4">
@@ -165,5 +343,44 @@ function getRoleLabel(role: string) {
</form> </form>
</div> </div>
</div> </div>
<!-- Edit Modal -->
<div v-if="showEditModal && editingUser" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div class="card w-full max-w-md m-4">
<h2 class="text-xl font-semibold mb-6"> {{ t('app.edit') }}</h2>
<form @submit.prevent="saveUser" class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium mb-1">{{ t('auth.firstName') }} *</label>
<input v-model="editingUser.first_name" type="text" required class="input" />
</div>
<div>
<label class="block text-sm font-medium mb-1">{{ t('auth.lastName') }} *</label>
<input v-model="editingUser.last_name" type="text" required class="input" />
</div>
</div>
<div>
<label class="block text-sm font-medium mb-1">{{ t('auth.email') }}</label>
<input :value="editingUser.email" type="email" disabled class="input bg-gray-100 dark:bg-gray-700" />
</div>
<div>
<label class="block text-sm font-medium mb-1">{{ t('users.phone') }}</label>
<input v-model="editingUser.phone" type="tel" class="input" />
</div>
<div v-if="authStore.isChef && editingUser.role !== 'chef'">
<label class="block text-sm font-medium mb-1">{{ t('users.role') }}</label>
<select v-model="editingUser.role" class="input">
<option value="mitarbeiter">{{ t('users.roles.mitarbeiter') }}</option>
<option value="disponent">{{ t('users.roles.disponent') }}</option>
<option value="subunternehmer">Subunternehmer</option>
</select>
</div>
<div class="flex justify-end gap-3 pt-4">
<button type="button" class="btn btn-secondary" @click="showEditModal = false">{{ t('app.cancel') }}</button>
<button type="submit" class="btn btn-primary">{{ t('app.save') }}</button>
</div>
</form>
</div>
</div>
</div> </div>
</template> </template>