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:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { api } from '@/api'
|
||||
@@ -20,6 +20,11 @@ interface User {
|
||||
const users = ref<User[]>([])
|
||||
const loading = ref(true)
|
||||
const showCreateModal = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const selectedUsers = ref<string[]>([])
|
||||
const showEditModal = ref(false)
|
||||
const editingUser = ref<User | null>(null)
|
||||
|
||||
const newUser = ref({
|
||||
email: '',
|
||||
password: '',
|
||||
@@ -29,6 +34,22 @@ const newUser = ref({
|
||||
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 () => {
|
||||
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) {
|
||||
const badges: Record<string, string> = {
|
||||
chef: 'badge-danger',
|
||||
disponent: 'badge-primary',
|
||||
mitarbeiter: 'badge-success'
|
||||
mitarbeiter: 'badge-success',
|
||||
subunternehmer: 'badge-warning'
|
||||
}
|
||||
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>
|
||||
</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 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">
|
||||
<thead>
|
||||
<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.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('app.status') }}</th>
|
||||
<th class="pb-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="user in users" :key="user.id" class="border-b border-gray-100 dark:border-gray-800">
|
||||
<td class="py-3 font-medium">{{ user.first_name }} {{ user.last_name }}</td>
|
||||
<td class="py-3 text-gray-500">{{ 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 ? t('users.active') : t('users.inactive') }}</span></td>
|
||||
<td class="py-3 text-right">
|
||||
<tr
|
||||
v-for="user in filteredUsers"
|
||||
:key="user.id"
|
||||
class="border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer"
|
||||
@click="openEditModal(user)"
|
||||
>
|
||||
<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
|
||||
v-if="user.id !== authStore.user?.id && user.role !== 'chef'"
|
||||
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">
|
||||
<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">
|
||||
@@ -165,5 +343,44 @@ function getRoleLabel(role: string) {
|
||||
</form>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user